<?xml version="1.0" encoding="UTF-8"?>
<rss version="2.0" xmlns:atom="http://www.w3.org/2005/Atom" xmlns:dc="http://purl.org/dc/elements/1.1/">
  <channel>
    <title>DEV Community: Mike Fallows</title>
    <description>The latest articles on DEV Community by Mike Fallows (@mikenewbuild).</description>
    <link>https://dev.to/mikenewbuild</link>
    <image>
      <url>https://media2.dev.to/dynamic/image/width=90,height=90,fit=cover,gravity=auto,format=auto/https:%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Fuser%2Fprofile_image%2F1047151%2Fabbf9eeb-2aab-402c-bb03-4562d44396ce.png</url>
      <title>DEV Community: Mike Fallows</title>
      <link>https://dev.to/mikenewbuild</link>
    </image>
    <atom:link rel="self" type="application/rss+xml" href="https://dev.to/feed/mikenewbuild"/>
    <language>en</language>
    <item>
      <title>Optimising CSS minification in Liquid</title>
      <dc:creator>Mike Fallows</dc:creator>
      <pubDate>Tue, 24 Oct 2023 23:00:00 +0000</pubDate>
      <link>https://dev.to/mikenewbuild/optimising-css-minification-in-liquid-2nlb</link>
      <guid>https://dev.to/mikenewbuild/optimising-css-minification-in-liquid-2nlb</guid>
      <description>&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're looking for good content on those subjects, I definitely recommend &lt;a href="https://ionlyspeakliquid.beehiiv.com/subscribe"&gt;subscribing&lt;/a&gt;.&lt;/p&gt;

&lt;p&gt;One tip mentioned in &lt;a href="https://ionlyspeakliquid.beehiiv.com/p/speak-liquid-33-minify-css-liquid"&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="https://community.shopify.com/c/technical-q-a/tool-to-minify-css-liquid/m-p/1123337"&gt;this forum post&lt;/a&gt;. It'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's a low effort way to increase a site'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's happening):&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight liquid"&gt;&lt;code&gt;&lt;span class="p"&gt;{%&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nt"&gt;capture&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nv"&gt;bloated&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;%}&lt;/span&gt;
/* CSS ... */
&lt;span class="p"&gt;{%&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nt"&gt;endcapture&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;%}&lt;/span&gt;

&amp;lt;!-- CSS &lt;span class="p"&gt;{{&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nv"&gt;bloated&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;size&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;}}&lt;/span&gt; chars --&amp;gt;
&lt;span class="p"&gt;{%-&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nt"&gt;liquid&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;assign&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="na"&gt;original&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s1"&gt;''&lt;/span&gt;&lt;span class="err"&gt;
&lt;/span&gt;&lt;span class="w"&gt;  &lt;/span&gt;#&lt;span class="w"&gt; &lt;/span&gt;remove&lt;span class="w"&gt; &lt;/span&gt;line&lt;span class="w"&gt; &lt;/span&gt;breaks,&lt;span class="err"&gt;
&lt;/span&gt;&lt;span class="w"&gt;  &lt;/span&gt;#&lt;span class="w"&gt; &lt;/span&gt;multiple&lt;span class="w"&gt; &lt;/span&gt;whitespace&lt;span class="w"&gt; &lt;/span&gt;characters&lt;span class="w"&gt; &lt;/span&gt;and&lt;span class="err"&gt;
&lt;/span&gt;&lt;span class="w"&gt;  &lt;/span&gt;#&lt;span class="w"&gt; &lt;/span&gt;split&lt;span class="w"&gt; &lt;/span&gt;on&lt;span class="w"&gt; &lt;/span&gt;closing&lt;span class="w"&gt; &lt;/span&gt;comment&lt;span class="w"&gt; &lt;/span&gt;tags&lt;span class="err"&gt;
&lt;/span&gt;&lt;span class="w"&gt;  &lt;/span&gt;assign&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="na"&gt;chunks&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;bloated&lt;span class="w"&gt; &lt;/span&gt;|&lt;span class="w"&gt; &lt;/span&gt;strip_newlines&lt;span class="w"&gt; &lt;/span&gt;|&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="na"&gt;split&lt;/span&gt;&lt;span class="o"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s1"&gt;' '&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;|&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="na"&gt;join&lt;/span&gt;&lt;span class="o"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s1"&gt;' '&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;|&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="na"&gt;split&lt;/span&gt;&lt;span class="o"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s1"&gt;'*/'&lt;/span&gt;&lt;span class="err"&gt;
&lt;/span&gt;&lt;span class="w"&gt;  &lt;/span&gt;#&lt;span class="w"&gt; &lt;/span&gt;iterate&lt;span class="w"&gt; &lt;/span&gt;over&lt;span class="w"&gt; &lt;/span&gt;each&lt;span class="w"&gt; &lt;/span&gt;chunk&lt;span class="w"&gt; &lt;/span&gt;of&lt;span class="w"&gt; &lt;/span&gt;CSS&lt;span class="err"&gt;
&lt;/span&gt;&lt;span class="w"&gt;  &lt;/span&gt;for&lt;span class="w"&gt; &lt;/span&gt;chunk&lt;span class="w"&gt; &lt;/span&gt;in&lt;span class="w"&gt; &lt;/span&gt;chunks&lt;span class="err"&gt;
&lt;/span&gt;&lt;span class="w"&gt;    &lt;/span&gt;#&lt;span class="w"&gt; &lt;/span&gt;remove&lt;span class="w"&gt; &lt;/span&gt;comments&lt;span class="w"&gt; &lt;/span&gt;and&lt;span class="w"&gt; &lt;/span&gt;whitespace&lt;span class="w"&gt; &lt;/span&gt;around&lt;span class="w"&gt; &lt;/span&gt;syntax&lt;span class="w"&gt; &lt;/span&gt;characters&lt;span class="err"&gt;
&lt;/span&gt;&lt;span class="w"&gt;    &lt;/span&gt;assign&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="na"&gt;mini&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;chunk&lt;span class="w"&gt; &lt;/span&gt;|&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="na"&gt;split&lt;/span&gt;&lt;span class="o"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s1"&gt;'/*'&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;|&lt;span class="w"&gt; &lt;/span&gt;first&lt;span class="w"&gt; &lt;/span&gt;|&lt;span class="w"&gt; &lt;/span&gt;strip&lt;span class="w"&gt; &lt;/span&gt;|&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="na"&gt;replace&lt;/span&gt;&lt;span class="o"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s1"&gt;'; '&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s1"&gt;';'&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;|&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="na"&gt;replace&lt;/span&gt;&lt;span class="o"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s1"&gt;'} '&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s1"&gt;'}'&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;|&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="na"&gt;replace&lt;/span&gt;&lt;span class="o"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s1"&gt;'{ '&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s1"&gt;'{'&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;|&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="na"&gt;replace&lt;/span&gt;&lt;span class="o"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s1"&gt;' {'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s1"&gt;'{'&lt;/span&gt;&lt;span class="err"&gt;
&lt;/span&gt;&lt;span class="w"&gt;    &lt;/span&gt;assign&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="na"&gt;original&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;original&lt;span class="w"&gt; &lt;/span&gt;|&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="na"&gt;append&lt;/span&gt;&lt;span class="o"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;mini&lt;span class="err"&gt;
&lt;/span&gt;&lt;span class="w"&gt;  &lt;/span&gt;endfor&lt;span class="err"&gt;
&lt;/span&gt;&lt;span class="w"&gt;  &lt;/span&gt;#&lt;span class="w"&gt; &lt;/span&gt;calculate&lt;span class="w"&gt; &lt;/span&gt;characters&lt;span class="w"&gt; &lt;/span&gt;saved&lt;span class="err"&gt;
&lt;/span&gt;&lt;span class="w"&gt;  &lt;/span&gt;assign&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="na"&gt;change&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;original.size&lt;span class="w"&gt; &lt;/span&gt;|&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="na"&gt;times&lt;/span&gt;&lt;span class="o"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mf"&gt;1.0&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;|&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="na"&gt;divided_by&lt;/span&gt;&lt;span class="o"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;bloated.size&lt;span class="w"&gt; &lt;/span&gt;|&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="na"&gt;times&lt;/span&gt;&lt;span class="o"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mf"&gt;100.0&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;|&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="na"&gt;round&lt;/span&gt;&lt;span class="o"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="err"&gt;
&lt;/span&gt;&lt;span class="p"&gt;%}&lt;/span&gt;
&amp;lt;!-- original -&lt;span class="p"&gt;{{&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nv"&gt;bloated&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;size&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;|&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nf"&gt;minus&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nv"&gt;original&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;size&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;}}&lt;/span&gt; &lt;span class="p"&gt;{{&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mi"&gt;100&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;|&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nf"&gt;minus&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nv"&gt;change&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;}}&lt;/span&gt;% --&amp;gt;

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

&lt;/div&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="https://css-tricks.com/css-basics-syntax-matters-syntax-doesnt/#aa-mostly-important-semicolons"&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: ';}', '}'&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;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight liquid"&gt;&lt;code&gt;&lt;span class="p"&gt;{%&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nt"&gt;capture&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nv"&gt;bloated&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;%}&lt;/span&gt;
// 5000+ lines of CSS
&lt;span class="p"&gt;{%&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nt"&gt;endcapture&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;%}&lt;/span&gt;

&amp;lt;!-- CSS &lt;span class="p"&gt;{{&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nv"&gt;bloated&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;size&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;}}&lt;/span&gt; chars --&amp;gt;
&lt;span class="p"&gt;{%-&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nt"&gt;liquid&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;assign&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="na"&gt;original&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s1"&gt;''&lt;/span&gt;&lt;span class="err"&gt;
&lt;/span&gt;&lt;span class="w"&gt;  &lt;/span&gt;assign&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="na"&gt;chunks&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;bloated&lt;span class="w"&gt; &lt;/span&gt;|&lt;span class="w"&gt; &lt;/span&gt;strip_newlines&lt;span class="w"&gt; &lt;/span&gt;|&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="na"&gt;split&lt;/span&gt;&lt;span class="o"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s1"&gt;' '&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;|&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="na"&gt;join&lt;/span&gt;&lt;span class="o"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s1"&gt;' '&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;|&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="na"&gt;split&lt;/span&gt;&lt;span class="o"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s1"&gt;'*/'&lt;/span&gt;&lt;span class="err"&gt;
&lt;/span&gt;&lt;span class="w"&gt;  &lt;/span&gt;for&lt;span class="w"&gt; &lt;/span&gt;chunk&lt;span class="w"&gt; &lt;/span&gt;in&lt;span class="w"&gt; &lt;/span&gt;chunks&lt;span class="err"&gt;
&lt;/span&gt;&lt;span class="w"&gt;    &lt;/span&gt;assign&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="na"&gt;mini&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;chunk&lt;span class="w"&gt; &lt;/span&gt;|&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="na"&gt;split&lt;/span&gt;&lt;span class="o"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s1"&gt;'/*'&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;|&lt;span class="w"&gt; &lt;/span&gt;first&lt;span class="w"&gt; &lt;/span&gt;|&lt;span class="w"&gt; &lt;/span&gt;strip&lt;span class="w"&gt; &lt;/span&gt;|&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="na"&gt;replace&lt;/span&gt;&lt;span class="o"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s1"&gt;'; '&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s1"&gt;';'&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;|&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="na"&gt;replace&lt;/span&gt;&lt;span class="o"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s1"&gt;'} '&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s1"&gt;'}'&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;|&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="na"&gt;replace&lt;/span&gt;&lt;span class="o"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s1"&gt;'{ '&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s1"&gt;'{'&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;|&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="na"&gt;replace&lt;/span&gt;&lt;span class="o"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s1"&gt;' {'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s1"&gt;'{'&lt;/span&gt;&lt;span class="err"&gt;
&lt;/span&gt;&lt;span class="w"&gt;    &lt;/span&gt;assign&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="na"&gt;original&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;original&lt;span class="w"&gt; &lt;/span&gt;|&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="na"&gt;append&lt;/span&gt;&lt;span class="o"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;mini&lt;span class="err"&gt;
&lt;/span&gt;&lt;span class="w"&gt;  &lt;/span&gt;endfor&lt;span class="err"&gt;
&lt;/span&gt;&lt;span class="w"&gt;  &lt;/span&gt;assign&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="na"&gt;change&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;original.size&lt;span class="w"&gt; &lt;/span&gt;|&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="na"&gt;times&lt;/span&gt;&lt;span class="o"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mf"&gt;1.0&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;|&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="na"&gt;divided_by&lt;/span&gt;&lt;span class="o"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;bloated.size&lt;span class="w"&gt; &lt;/span&gt;|&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="na"&gt;times&lt;/span&gt;&lt;span class="o"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mf"&gt;100.0&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;|&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="na"&gt;round&lt;/span&gt;&lt;span class="o"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="err"&gt;
&lt;/span&gt;&lt;span class="p"&gt;%}&lt;/span&gt;
&amp;lt;!-- original -&lt;span class="p"&gt;{{&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nv"&gt;bloated&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;size&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;|&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nf"&gt;minus&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nv"&gt;original&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;size&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;}}&lt;/span&gt; &lt;span class="p"&gt;{{&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mi"&gt;100&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;|&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nf"&gt;minus&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nv"&gt;change&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;}}&lt;/span&gt;% --&amp;gt;

&lt;span class="p"&gt;{%-&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nt"&gt;liquid&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;assign&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="na"&gt;optimised&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s1"&gt;''&lt;/span&gt;&lt;span class="err"&gt;
&lt;/span&gt;&lt;span class="w"&gt;  &lt;/span&gt;assign&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="na"&gt;chunks&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;bloated&lt;span class="w"&gt; &lt;/span&gt;|&lt;span class="w"&gt; &lt;/span&gt;strip_newlines&lt;span class="w"&gt; &lt;/span&gt;|&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="na"&gt;split&lt;/span&gt;&lt;span class="o"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s1"&gt;' '&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;|&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="na"&gt;join&lt;/span&gt;&lt;span class="o"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s1"&gt;' '&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;|&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="na"&gt;split&lt;/span&gt;&lt;span class="o"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s1"&gt;'*/'&lt;/span&gt;&lt;span class="err"&gt;
&lt;/span&gt;&lt;span class="w"&gt;  &lt;/span&gt;for&lt;span class="w"&gt; &lt;/span&gt;chunk&lt;span class="w"&gt; &lt;/span&gt;in&lt;span class="w"&gt; &lt;/span&gt;chunks&lt;span class="err"&gt;
&lt;/span&gt;&lt;span class="w"&gt;    &lt;/span&gt;assign&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="na"&gt;mini&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;chunk&lt;span class="w"&gt; &lt;/span&gt;|&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="na"&gt;split&lt;/span&gt;&lt;span class="o"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s1"&gt;'/*'&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;|&lt;span class="w"&gt; &lt;/span&gt;first&lt;span class="w"&gt; &lt;/span&gt;|&lt;span class="w"&gt; &lt;/span&gt;strip&lt;span class="w"&gt; &lt;/span&gt;|&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="na"&gt;replace&lt;/span&gt;&lt;span class="o"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s1"&gt;': '&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s1"&gt;':'&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;|&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="na"&gt;replace&lt;/span&gt;&lt;span class="o"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s1"&gt;'; '&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s1"&gt;';'&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;|&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="na"&gt;replace&lt;/span&gt;&lt;span class="o"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s1"&gt;'} '&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s1"&gt;'}'&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;|&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="na"&gt;replace&lt;/span&gt;&lt;span class="o"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s1"&gt;'{ '&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s1"&gt;'{'&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;|&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="na"&gt;replace&lt;/span&gt;&lt;span class="o"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s1"&gt;' {'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s1"&gt;'{'&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;|&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="na"&gt;replace&lt;/span&gt;&lt;span class="o"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s1"&gt;';}'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s1"&gt;'}'&lt;/span&gt;&lt;span class="err"&gt;
&lt;/span&gt;&lt;span class="w"&gt;    &lt;/span&gt;assign&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="na"&gt;optimised&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;optimised&lt;span class="w"&gt; &lt;/span&gt;|&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="na"&gt;append&lt;/span&gt;&lt;span class="o"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;mini&lt;span class="err"&gt;
&lt;/span&gt;&lt;span class="w"&gt;  &lt;/span&gt;endfor&lt;span class="err"&gt;
&lt;/span&gt;&lt;span class="w"&gt;  &lt;/span&gt;assign&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="na"&gt;change&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;optimised.size&lt;span class="w"&gt; &lt;/span&gt;|&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="na"&gt;times&lt;/span&gt;&lt;span class="o"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mf"&gt;1.0&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;|&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="na"&gt;divided_by&lt;/span&gt;&lt;span class="o"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;bloated.size&lt;span class="w"&gt; &lt;/span&gt;|&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="na"&gt;times&lt;/span&gt;&lt;span class="o"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mf"&gt;100.0&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;|&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="na"&gt;round&lt;/span&gt;&lt;span class="o"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="err"&gt;
&lt;/span&gt;&lt;span class="p"&gt;%}&lt;/span&gt;
&amp;lt;!-- optimised -&lt;span class="p"&gt;{{&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nv"&gt;bloated&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;size&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;|&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nf"&gt;minus&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nv"&gt;optimised&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;size&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;}}&lt;/span&gt; &lt;span class="p"&gt;{{&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mi"&gt;100&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;|&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nf"&gt;minus&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nv"&gt;change&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;}}&lt;/span&gt;% --&amp;gt;

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

&lt;/div&gt;



&lt;h2&gt;
  
  
  Results
&lt;/h2&gt;

&lt;p&gt;Here are the results:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight html"&gt;&lt;code&gt;&lt;span class="c"&gt;&amp;lt;!-- CSS 121066 chars --&amp;gt;&lt;/span&gt;
&lt;span class="c"&gt;&amp;lt;!-- original -12829 10.6% --&amp;gt;&lt;/span&gt;
&lt;span class="c"&gt;&amp;lt;!-- optimised -16091 13.3% --&amp;gt;&lt;/span&gt;

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

&lt;/div&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;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight liquid"&gt;&lt;code&gt;&lt;span class="p"&gt;{%-&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nt"&gt;liquid&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;assign&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="na"&gt;chunks&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;input&lt;span class="w"&gt; &lt;/span&gt;|&lt;span class="w"&gt; &lt;/span&gt;strip_newlines&lt;span class="w"&gt; &lt;/span&gt;|&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="na"&gt;split&lt;/span&gt;&lt;span class="o"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s1"&gt;' '&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;|&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="na"&gt;join&lt;/span&gt;&lt;span class="o"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s1"&gt;' '&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;|&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="na"&gt;split&lt;/span&gt;&lt;span class="o"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s1"&gt;'*/'&lt;/span&gt;&lt;span class="err"&gt;
&lt;/span&gt;&lt;span class="w"&gt;  &lt;/span&gt;for&lt;span class="w"&gt; &lt;/span&gt;chunk&lt;span class="w"&gt; &lt;/span&gt;in&lt;span class="w"&gt; &lt;/span&gt;chunks&lt;span class="err"&gt;
&lt;/span&gt;&lt;span class="w"&gt;    &lt;/span&gt;assign&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="na"&gt;mini&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;chunk&lt;span class="w"&gt; &lt;/span&gt;|&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="na"&gt;split&lt;/span&gt;&lt;span class="o"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s1"&gt;'/*'&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;|&lt;span class="w"&gt; &lt;/span&gt;first&lt;span class="w"&gt; &lt;/span&gt;|&lt;span class="w"&gt; &lt;/span&gt;strip&lt;span class="w"&gt; &lt;/span&gt;|&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="na"&gt;replace&lt;/span&gt;&lt;span class="o"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s1"&gt;': '&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s1"&gt;':'&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;|&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="na"&gt;replace&lt;/span&gt;&lt;span class="o"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s1"&gt;'; '&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s1"&gt;';'&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;|&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="na"&gt;replace&lt;/span&gt;&lt;span class="o"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s1"&gt;'} '&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s1"&gt;'}'&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;|&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="na"&gt;replace&lt;/span&gt;&lt;span class="o"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s1"&gt;'{ '&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s1"&gt;'{'&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;|&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="na"&gt;replace&lt;/span&gt;&lt;span class="o"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s1"&gt;' {'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s1"&gt;'{'&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;|&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="na"&gt;replace&lt;/span&gt;&lt;span class="o"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s1"&gt;';}'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s1"&gt;'}'&lt;/span&gt;&lt;span class="err"&gt;
&lt;/span&gt;&lt;span class="w"&gt;    &lt;/span&gt;echo&lt;span class="w"&gt; &lt;/span&gt;mini&lt;span class="err"&gt;
&lt;/span&gt;&lt;span class="w"&gt;  &lt;/span&gt;endfor&lt;span class="err"&gt;
&lt;/span&gt;&lt;span class="p"&gt;%}&lt;/span&gt;

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

&lt;/div&gt;



&lt;p&gt;So it can even be used like this:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight liquid"&gt;&lt;code&gt;&amp;lt;style&amp;gt;
&lt;span class="p"&gt;{%-&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nt"&gt;capture&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nv"&gt;bloated&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;%}&lt;/span&gt;
// Unminified CSS
&lt;span class="p"&gt;{%-&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nt"&gt;endcapture&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;%}&lt;/span&gt;

&lt;span class="p"&gt;{%-&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nt"&gt;render&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s1"&gt;'minify-css'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="na"&gt;input&lt;/span&gt;&lt;span class="o"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nv"&gt;bloated&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;-%}&lt;/span&gt;
&amp;lt;/style&amp;gt;

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

&lt;/div&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;

</description>
      <category>shopify</category>
      <category>css</category>
    </item>
    <item>
      <title>Prefill a Shopify discount with the Fetch API</title>
      <dc:creator>Mike Fallows</dc:creator>
      <pubDate>Mon, 23 Oct 2023 10:00:00 +0000</pubDate>
      <link>https://dev.to/mikenewbuild/prefill-a-shopify-discount-with-the-fetch-api-51ob</link>
      <guid>https://dev.to/mikenewbuild/prefill-a-shopify-discount-with-the-fetch-api-51ob</guid>
      <description>&lt;p&gt;Shopify has a method for &lt;a href="https://help.shopify.com/en/manual/discounts/managing-discount-codes#promote-a-discount-using-a-shareable-link"&gt;prefilling discounts at checkout&lt;/a&gt;. It'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&lt;sup id="fnref1"&gt;1&lt;/sup&gt; 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="https://help.shopify.com/en/manual/discounts/discount-types#automatic-discounts"&gt;Automatic Discounts&lt;/a&gt; that don'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'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 'visit' 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'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's the code with section settings that allow it to be added to your OS2 templates.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight liquid"&gt;&lt;code&gt;&lt;span class="p"&gt;{%-&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="kr"&gt;if&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nv"&gt;section&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nv"&gt;settings&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nv"&gt;discount_code&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="o"&gt;!=&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nb"&gt;blank&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;-%}&lt;/span&gt;
&amp;lt;script&amp;gt;fetch('/discount/&lt;span class="p"&gt;{{&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nv"&gt;section&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nv"&gt;settings&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nv"&gt;discount_code&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;}}&lt;/span&gt;');&amp;lt;/script&amp;gt;
&lt;span class="p"&gt;{%-&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="kr"&gt;endif&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;-%}&lt;/span&gt;

&lt;span class="p"&gt;{%&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nt"&gt;schema&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;%}&lt;/span&gt;
{
  "name": "Prefill discount",
  "class": "shopify-section--prefill-discount",
  "tag": "section",
  "settings": [
    {
      "type": "text",
      "id": "discount_code",
      "label": "Discount code"
    }
  ],
  "presets": [
    {
      "name": "Prefill discount"
    }
  ]
}
&lt;span class="p"&gt;{%&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nt"&gt;endschema&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;%}&lt;/span&gt;

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

&lt;/div&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;ol&gt;

&lt;li id="fn1"&gt;
&lt;p&gt;Actually, it'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="https://help.shopify.com/en/manual/discounts/managing-discount-codes#promote-a-discount-using-a-shareable-link"&gt;in the docs&lt;/a&gt;.&amp;nbsp;↩&lt;/p&gt;
&lt;/li&gt;

&lt;/ol&gt;

</description>
      <category>shopify</category>
      <category>javascript</category>
    </item>
    <item>
      <title>Implement a low stock notice in a Shopify theme</title>
      <dc:creator>Mike Fallows</dc:creator>
      <pubDate>Thu, 19 Oct 2023 23:00:00 +0000</pubDate>
      <link>https://dev.to/mikenewbuild/implement-a-low-stock-notice-in-a-shopify-theme-2la1</link>
      <guid>https://dev.to/mikenewbuild/implement-a-low-stock-notice-in-a-shopify-theme-2la1</guid>
      <description>&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. "3 in stock".&lt;/p&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'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.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight javascript"&gt;&lt;code&gt;&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;inventory&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;
  &lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="o"&gt;%-&lt;/span&gt; &lt;span class="k"&gt;for&lt;/span&gt; &lt;span class="nx"&gt;v&lt;/span&gt; &lt;span class="k"&gt;in&lt;/span&gt; &lt;span class="nx"&gt;product&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;variants&lt;/span&gt; &lt;span class="o"&gt;%&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;
    &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="na"&gt;id&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;{{ v.id }}&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="na"&gt;quantity&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;{{ v.inventory_quantity }}&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt; &lt;span class="p"&gt;},&lt;/span&gt;
  &lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="o"&gt;%-&lt;/span&gt; &lt;span class="nx"&gt;endfor&lt;/span&gt; &lt;span class="o"&gt;%&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;
&lt;span class="p"&gt;];&lt;/span&gt;

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

&lt;/div&gt;



&lt;p&gt;Assuming that the HTML structure of the page is something that could be boiled down to a simplified structure like this:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight html"&gt;&lt;code&gt;&lt;span class="nt"&gt;&amp;lt;div&lt;/span&gt; &lt;span class="na"&gt;class=&lt;/span&gt;&lt;span class="s"&gt;"product"&lt;/span&gt;&lt;span class="nt"&gt;&amp;gt;&lt;/span&gt;
  &lt;span class="nt"&gt;&amp;lt;form&amp;gt;&lt;/span&gt;
    &lt;span class="nt"&gt;&amp;lt;input&lt;/span&gt; &lt;span class="na"&gt;type=&lt;/span&gt;&lt;span class="s"&gt;"hidden"&lt;/span&gt; &lt;span class="na"&gt;class=&lt;/span&gt;&lt;span class="s"&gt;"product-variant-id"&lt;/span&gt; &lt;span class="na"&gt;value=&lt;/span&gt;&lt;span class="s"&gt;"123"&lt;/span&gt;&lt;span class="nt"&gt;&amp;gt;&lt;/span&gt;
    &lt;span class="nt"&gt;&amp;lt;select&lt;/span&gt; &lt;span class="na"&gt;name=&lt;/span&gt;&lt;span class="s"&gt;"id"&lt;/span&gt; &lt;span class="na"&gt;class=&lt;/span&gt;&lt;span class="s"&gt;"quantity"&lt;/span&gt;&lt;span class="nt"&gt;&amp;gt;&lt;/span&gt;
      &lt;span class="nt"&gt;&amp;lt;option&lt;/span&gt; &lt;span class="na"&gt;value=&lt;/span&gt;&lt;span class="s"&gt;"123"&lt;/span&gt;&lt;span class="nt"&gt;&amp;gt;&lt;/span&gt;Standard&lt;span class="nt"&gt;&amp;lt;/option&amp;gt;&lt;/span&gt;
      &lt;span class="nt"&gt;&amp;lt;option&lt;/span&gt; &lt;span class="na"&gt;value=&lt;/span&gt;&lt;span class="s"&gt;"456"&lt;/span&gt;&lt;span class="nt"&gt;&amp;gt;&lt;/span&gt;Deluxe&lt;span class="nt"&gt;&amp;lt;/option&amp;gt;&lt;/span&gt;
    &lt;span class="nt"&gt;&amp;lt;/select&amp;gt;&lt;/span&gt;
  &lt;span class="nt"&gt;&amp;lt;/form&amp;gt;&lt;/span&gt;
&lt;span class="nt"&gt;&amp;lt;/div&amp;gt;&lt;/span&gt;

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

&lt;/div&gt;



&lt;p&gt;Then the script would look like this:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight html"&gt;&lt;code&gt;&lt;span class="nt"&gt;&amp;lt;script&amp;gt;&lt;/span&gt;
  &lt;span class="nb"&gt;window&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;addEventListener&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;DOMContentLoaded&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;

    &lt;span class="c1"&gt;// Initial config&lt;/span&gt;
    &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;threshold&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="mi"&gt;5&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
    &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;productSelector&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;.product&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
    &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;quantitySelector&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;.quantity&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
    &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;hiddenVariantIdInputSelector&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;.product-variant-id&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
    &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;initialVariantId&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;{{ product.selected_or_first_available_variant.id }}&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

    &lt;span class="c1"&gt;// Store stock quantities&lt;/span&gt;
    &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;inventory&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;
      &lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="o"&gt;%-&lt;/span&gt; &lt;span class="k"&gt;for&lt;/span&gt; &lt;span class="nx"&gt;v&lt;/span&gt; &lt;span class="k"&gt;in&lt;/span&gt; &lt;span class="nx"&gt;product&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;variants&lt;/span&gt; &lt;span class="o"&gt;%&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;
        &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="na"&gt;id&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{{&lt;/span&gt; &lt;span class="nx"&gt;v&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;id&lt;/span&gt; &lt;span class="o"&gt;|&lt;/span&gt; &lt;span class="nx"&gt;json&lt;/span&gt; &lt;span class="p"&gt;}},&lt;/span&gt; &lt;span class="na"&gt;quantity&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{{&lt;/span&gt; &lt;span class="nx"&gt;v&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;inventory_quantity&lt;/span&gt; &lt;span class="o"&gt;|&lt;/span&gt; &lt;span class="nx"&gt;json&lt;/span&gt; &lt;span class="p"&gt;}}&lt;/span&gt; &lt;span class="p"&gt;},&lt;/span&gt;
      &lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="o"&gt;%-&lt;/span&gt; &lt;span class="nx"&gt;endfor&lt;/span&gt; &lt;span class="o"&gt;%&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;
    &lt;span class="p"&gt;];&lt;/span&gt;

    &lt;span class="c1"&gt;// Find the root product element&lt;/span&gt;
    &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;productEl&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nb"&gt;document&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;querySelector&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;productSelector&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
    &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="o"&gt;!&lt;/span&gt;&lt;span class="nx"&gt;productEl&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;return&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

    &lt;span class="c1"&gt;// Find the quantity selector (we will append our low stock notice container to this)&lt;/span&gt;
    &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;quantityEl&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;productEl&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;querySelector&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;quantitySelector&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
    &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="o"&gt;!&lt;/span&gt;&lt;span class="nx"&gt;quantityEl&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;return&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

    &lt;span class="c1"&gt;// Append the notice container&lt;/span&gt;
    &lt;span class="nx"&gt;quantityEl&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;insertAdjacentHTML&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;afterend&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s2"&gt;`&amp;lt;div class="stock-notice"&amp;gt;&amp;lt;/div&amp;gt;`&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;

    &lt;span class="c1"&gt;// Method to update the content of the notice container&lt;/span&gt;
    &lt;span class="c1"&gt;// if the stock is not above the threshold&lt;/span&gt;
    &lt;span class="kd"&gt;function&lt;/span&gt; &lt;span class="nx"&gt;updateMessage&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;variantId&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
      &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;stockNoticeEl&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;productEl&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;querySelector&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;.stock-notice&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
      &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="o"&gt;!&lt;/span&gt;&lt;span class="nx"&gt;stockNoticeEl&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;return&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
      &lt;span class="kd"&gt;let&lt;/span&gt; &lt;span class="nx"&gt;message&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="dl"&gt;''&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
      &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;variant&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;inventory&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;find&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;i&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="nx"&gt;i&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;id&lt;/span&gt; &lt;span class="o"&gt;===&lt;/span&gt; &lt;span class="nx"&gt;variantId&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
      &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;variant&lt;/span&gt; &lt;span class="o"&gt;&amp;amp;&amp;amp;&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;variant&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;quantity&lt;/span&gt; &lt;span class="o"&gt;&amp;gt;&lt;/span&gt; &lt;span class="mi"&gt;0&lt;/span&gt; &lt;span class="o"&gt;&amp;amp;&amp;amp;&lt;/span&gt; &lt;span class="nx"&gt;variant&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;quantity&lt;/span&gt; &lt;span class="o"&gt;&amp;lt;=&lt;/span&gt; &lt;span class="nx"&gt;threshold&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="nx"&gt;message&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="s2"&gt;`&lt;/span&gt;&lt;span class="p"&gt;${&lt;/span&gt;&lt;span class="nx"&gt;variant&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;quantity&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="s2"&gt; in stock`&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
      &lt;span class="p"&gt;}&lt;/span&gt;
      &lt;span class="nx"&gt;stockNoticeEl&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;textContent&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;message&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt;

    &lt;span class="c1"&gt;// Update the message on first page load&lt;/span&gt;
    &lt;span class="nx"&gt;updateMessage&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;initialVariantId&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;

    &lt;span class="c1"&gt;// Find the input that is updated with the selected id&lt;/span&gt;
    &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;hiddenVariantIdInputEl&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;productEl&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;querySelector&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;hiddenVariantIdInputSelector&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
    &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="o"&gt;!&lt;/span&gt;&lt;span class="nx"&gt;hiddenVariantIdInputEl&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;return&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

    &lt;span class="c1"&gt;// Watch the input for changes and update the message in response&lt;/span&gt;
    &lt;span class="nx"&gt;hiddenVariantIdInputEl&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;addEventListener&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;change&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;e&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="nx"&gt;updateMessage&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;e&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;target&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;value&lt;/span&gt;&lt;span class="p"&gt;));&lt;/span&gt;

  &lt;span class="p"&gt;});&lt;/span&gt;
&lt;span class="nt"&gt;&amp;lt;/script&amp;gt;&lt;/span&gt;

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

&lt;/div&gt;



&lt;h2&gt;
  
  
  Customisation
&lt;/h2&gt;

&lt;p&gt;You could add the inventory threshold as a section setting. That would also allow you to populate the threshold through a value stored on the product as a metafield 🙌&lt;/p&gt;

</description>
      <category>shopify</category>
      <category>javascript</category>
    </item>
    <item>
      <title>Quick error reporting with Val Town</title>
      <dc:creator>Mike Fallows</dc:creator>
      <pubDate>Mon, 21 Aug 2023 12:00:00 +0000</pubDate>
      <link>https://dev.to/mikenewbuild/quick-error-reporting-with-val-town-154h</link>
      <guid>https://dev.to/mikenewbuild/quick-error-reporting-with-val-town-154h</guid>
      <description>&lt;p&gt;Recently I needed to quickly inspect a script running in the 'Additional Checkout Scripts' portion of the order thank you / status page for a Shopify client. These are copy-paste scripts and there's no easy way to manage them, and it'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'd recently heard about the features of &lt;a href="https://val.town/"&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'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's create a store:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight javascript"&gt;&lt;code&gt;&lt;span class="kd"&gt;let&lt;/span&gt; &lt;span class="nx"&gt;store&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;[];&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Yep, that's literally it. Val Town will let you add up to 100kb of data to that value (on it's very reasonable free tier), at which point it will begin to truncate the data. This is plenty for my purposes as I'm only storing an order id and at most I'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'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 the &lt;code&gt;@me&lt;/code&gt; shorthand for referencing your own Vals and also that it's constructed as an IIFE to make sure that the function is invoked when you 'run' the Val. A cool feature of Val Town is that you can easily reference other people's Val's with the &lt;code&gt;@&lt;/code&gt; syntax, but I think that &lt;a href="https://blog.val.town/blog/val-town-runtime-v3-my-mistakes-were-easy-the-solutions-simple"&gt;it will be going away&lt;/a&gt; at some point in favour of standard import statements which is even cooler.&lt;/p&gt;

&lt;p&gt;Just beware that when you run this Val, the results may not appear instantly (ie. your store won't be cleared out straight away so wait a few moments to see the results).&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight javascript"&gt;&lt;code&gt;&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;clear&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="k"&gt;async&lt;/span&gt; &lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="p"&gt;@&lt;/span&gt;&lt;span class="nd"&gt;me&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;store&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;[];&lt;/span&gt;
&lt;span class="p"&gt;})();&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;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'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;console.email&lt;/code&gt; global 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;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight javascript"&gt;&lt;code&gt;&lt;span class="kd"&gt;function&lt;/span&gt; &lt;span class="nx"&gt;report&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;subject&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;message&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;error&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="kc"&gt;false&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="p"&gt;@&lt;/span&gt;&lt;span class="nd"&gt;me&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;store&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;push&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt; &lt;span class="nx"&gt;subject&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;message&lt;/span&gt; &lt;span class="p"&gt;});&lt;/span&gt;

  &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;error&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="nx"&gt;console&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;email&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt;
      &lt;span class="na"&gt;subject&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="s2"&gt;`Error: &lt;/span&gt;&lt;span class="p"&gt;${&lt;/span&gt;&lt;span class="nx"&gt;subject&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;`&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
      &lt;span class="na"&gt;html&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="s2"&gt;`&amp;lt;pre&amp;gt;&lt;/span&gt;&lt;span class="p"&gt;${&lt;/span&gt;&lt;span class="nx"&gt;message&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;&amp;lt;/pre&amp;gt;&amp;lt;hr&amp;gt;&amp;lt;pre&amp;gt;&lt;/span&gt;&lt;span class="p"&gt;${&lt;/span&gt;&lt;span class="nx"&gt;error&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;&amp;lt;/pre&amp;gt;`&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="p"&gt;});&lt;/span&gt;
  &lt;span class="p"&gt;}&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Remember to click the lock icon on the Val to convert from a 'private' Val. Private Val's need an API key that you wouldn'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's not enough for general purpose, so just keep this in mind. I'm certainly not storing or transferring any sensitive data (only an order id reference), and I'm aware that the endpoint could be abused to spam me at which point I'd have to take measures to defend against that if it were the case (like, change the Val's endpoint?). If you make the Val 'public' (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'm storing the strings for the subject and message data I want to store (Shopify's liquid syntax will replace the &lt;code&gt;and&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;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;let report = [
  `Order on {{ shop.url }}`,
  `order_id: {{ order_id }}`,
];

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

fetch("https://api.val.town/v1/run/&amp;lt;username&amp;gt;.report?args=" + JSON.stringify(report));
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&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 'Interval' 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;

</description>
      <category>javascript</category>
    </item>
    <item>
      <title>Observing cart changes in a Shopify theme</title>
      <dc:creator>Mike Fallows</dc:creator>
      <pubDate>Mon, 06 Mar 2023 00:00:00 +0000</pubDate>
      <link>https://dev.to/mikenewbuild/observing-cart-changes-in-a-shopify-theme-1nji</link>
      <guid>https://dev.to/mikenewbuild/observing-cart-changes-in-a-shopify-theme-1nji</guid>
      <description>&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 'Ajax' 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've often seen crude attempts to solve this by adding event listeners to the 'Add to cart' button on the product page. But this falls down once you consider things like 'Quick Buy' or 'Upsell' features, or take into account that simply clicking an 'Add to cart' button doesn'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;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight javascript"&gt;&lt;code&gt;&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;page_type&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;{{&lt;/span&gt; &lt;span class="nx"&gt;request&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;page_type&lt;/span&gt; &lt;span class="o"&gt;|&lt;/span&gt; &lt;span class="nx"&gt;json&lt;/span&gt; &lt;span class="p"&gt;}};&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;It'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;&lt;sup id="fnref1"&gt;1&lt;/sup&gt;
&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;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight javascript"&gt;&lt;code&gt;&lt;span class="kd"&gt;function&lt;/span&gt; &lt;span class="nx"&gt;observeCartChanges&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;cartObserver&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nx"&gt;PerformanceObserver&lt;/span&gt;&lt;span class="p"&gt;((&lt;/span&gt;&lt;span class="nx"&gt;list&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="nx"&gt;list&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;getEntries&lt;/span&gt;&lt;span class="p"&gt;().&lt;/span&gt;&lt;span class="nx"&gt;forEach&lt;/span&gt;&lt;span class="p"&gt;((&lt;/span&gt;&lt;span class="nx"&gt;entry&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
      &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;isValidRequestType&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;xmlhttprequest&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;fetch&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;].&lt;/span&gt;&lt;span class="nx"&gt;includes&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;entry&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;initiatorType&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
      &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;isCartChangeRequest&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="sr"&gt;/&lt;/span&gt;&lt;span class="se"&gt;\/&lt;/span&gt;&lt;span class="sr"&gt;cart&lt;/span&gt;&lt;span class="se"&gt;\/&lt;/span&gt;&lt;span class="sr"&gt;/&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;test&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;entry&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;name&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
      &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;isValidRequestType&lt;/span&gt; &lt;span class="o"&gt;&amp;amp;&amp;amp;&lt;/span&gt; &lt;span class="nx"&gt;isCartChangeRequest&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="c1"&gt;// handle cart request&lt;/span&gt;
      &lt;span class="p"&gt;}&lt;/span&gt;
    &lt;span class="p"&gt;});&lt;/span&gt;
  &lt;span class="p"&gt;});&lt;/span&gt;
  &lt;span class="nx"&gt;cartObserver&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;observe&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt; &lt;span class="na"&gt;entryTypes&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;resource&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="p"&gt;});&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&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's a quick example of fetching the current cart, as well as storing and then retrieving it from local storage:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight javascript"&gt;&lt;code&gt;&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;response&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nx"&gt;fetch&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;/cart.js&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;cart&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;response&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;json&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;
&lt;span class="nx"&gt;localStorage&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;setItem&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;cart&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;JSON&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;stringify&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;cart&lt;/span&gt;&lt;span class="p"&gt;));&lt;/span&gt;
&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;storedCart&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;JSON&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;parse&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;localStorage&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;getItem&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;cart&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;));&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&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'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'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's an example of how that function could look:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight javascript"&gt;&lt;code&gt;&lt;span class="kd"&gt;function&lt;/span&gt; &lt;span class="nx"&gt;findCartChanges&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;oldCart&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;newCart&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;onlyInLeft&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;l&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;r&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="nx"&gt;l&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;filter&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;li&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="o"&gt;!&lt;/span&gt;&lt;span class="nx"&gt;r&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;some&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;ri&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="nx"&gt;li&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;key&lt;/span&gt; &lt;span class="o"&gt;==&lt;/span&gt; &lt;span class="nx"&gt;ri&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;key&lt;/span&gt;&lt;span class="p"&gt;));&lt;/span&gt;
  &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="na"&gt;added&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;onlyInLeft&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;newCart&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;items&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;oldCart&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;items&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt;
    &lt;span class="na"&gt;removed&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;onlyInLeft&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;oldCart&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;items&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;newCart&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;items&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt;
  &lt;span class="p"&gt;};&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h3&gt;
  
  
  Identifying items that have been updated
&lt;/h3&gt;

&lt;p&gt;So far, so good. But this still doesn'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'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'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's how that looks:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight javascript"&gt;&lt;code&gt;&lt;span class="kd"&gt;function&lt;/span&gt; &lt;span class="nx"&gt;findCartChanges&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;oldCart&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;newCart&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;onlyInLeft&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;l&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;r&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="nx"&gt;l&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;filter&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;li&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="o"&gt;!&lt;/span&gt;&lt;span class="nx"&gt;r&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;some&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;ri&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="nx"&gt;li&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;key&lt;/span&gt; &lt;span class="o"&gt;==&lt;/span&gt; &lt;span class="nx"&gt;ri&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;key&lt;/span&gt;&lt;span class="p"&gt;));&lt;/span&gt;
  &lt;span class="kd"&gt;let&lt;/span&gt; &lt;span class="nx"&gt;result&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="na"&gt;added&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;onlyInLeft&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;newCart&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;items&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;oldCart&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;items&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt;
    &lt;span class="na"&gt;removed&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;onlyInLeft&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;oldCart&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;items&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;newCart&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;items&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt;
  &lt;span class="p"&gt;};&lt;/span&gt;

  &lt;span class="nx"&gt;oldCart&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;items&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;forEach&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;oi&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;ni&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;newCart&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;items&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;find&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;i&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="nx"&gt;i&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;key&lt;/span&gt; &lt;span class="o"&gt;==&lt;/span&gt; &lt;span class="nx"&gt;oi&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;key&lt;/span&gt; &lt;span class="o"&gt;&amp;amp;&amp;amp;&lt;/span&gt; &lt;span class="nx"&gt;i&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;quantity&lt;/span&gt; &lt;span class="o"&gt;!=&lt;/span&gt; &lt;span class="nx"&gt;oi&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;quantity&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
    &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="o"&gt;!&lt;/span&gt;&lt;span class="nx"&gt;ni&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;return&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
    &lt;span class="kd"&gt;let&lt;/span&gt; &lt;span class="nx"&gt;quantity&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;ni&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;quantity&lt;/span&gt; &lt;span class="o"&gt;-&lt;/span&gt; &lt;span class="nx"&gt;oi&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;quantity&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
    &lt;span class="kd"&gt;let&lt;/span&gt; &lt;span class="nx"&gt;item&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="p"&gt;...&lt;/span&gt;&lt;span class="nx"&gt;ni&lt;/span&gt; &lt;span class="p"&gt;};&lt;/span&gt;
    &lt;span class="nx"&gt;item&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;quantity&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nb"&gt;Math&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;abs&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;quantity&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
    &lt;span class="nx"&gt;quantity&lt;/span&gt; &lt;span class="o"&gt;&amp;gt;&lt;/span&gt; &lt;span class="mi"&gt;0&lt;/span&gt;
      &lt;span class="p"&gt;?&lt;/span&gt; &lt;span class="nx"&gt;result&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;added&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;push&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;item&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
      &lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;result&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;removed&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;push&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;item&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
  &lt;span class="p"&gt;});&lt;/span&gt;

  &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="nx"&gt;result&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h2&gt;
  
  
  Wrapping up
&lt;/h2&gt;

&lt;p&gt;Here'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;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight javascript"&gt;&lt;code&gt;&lt;span class="kd"&gt;class&lt;/span&gt; &lt;span class="nx"&gt;CartWatcher&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;

  &lt;span class="nx"&gt;init&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="k"&gt;this&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;emitCartChanges&lt;/span&gt;&lt;span class="p"&gt;().&lt;/span&gt;&lt;span class="nx"&gt;then&lt;/span&gt;&lt;span class="p"&gt;(()&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
      &lt;span class="k"&gt;this&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;observeCartChanges&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;
    &lt;span class="p"&gt;});&lt;/span&gt;
  &lt;span class="p"&gt;}&lt;/span&gt;

  &lt;span class="k"&gt;async&lt;/span&gt; &lt;span class="nx"&gt;fetchCart&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;response&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nx"&gt;fetch&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;/cart.js&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="nx"&gt;response&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;json&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;
  &lt;span class="p"&gt;}&lt;/span&gt;

  &lt;span class="nx"&gt;storeCart&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;cart&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="nx"&gt;localStorage&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;setItem&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;cart&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;JSON&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;stringify&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;cart&lt;/span&gt;&lt;span class="p"&gt;));&lt;/span&gt;
  &lt;span class="p"&gt;}&lt;/span&gt;

 &lt;span class="nx"&gt;storedCart&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="nx"&gt;JSON&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;parse&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;localStorage&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;getItem&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;cart&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt; &lt;span class="o"&gt;||&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="na"&gt;items&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;[]&lt;/span&gt; &lt;span class="p"&gt;};&lt;/span&gt;
  &lt;span class="p"&gt;}&lt;/span&gt;

 &lt;span class="nx"&gt;findCartChanges&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;oldCart&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;newCart&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;onlyInLeft&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;l&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;r&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="nx"&gt;l&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;filter&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;li&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="o"&gt;!&lt;/span&gt;&lt;span class="nx"&gt;r&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;some&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;ri&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="nx"&gt;li&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;key&lt;/span&gt; &lt;span class="o"&gt;==&lt;/span&gt; &lt;span class="nx"&gt;ri&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;key&lt;/span&gt;&lt;span class="p"&gt;));&lt;/span&gt;
    &lt;span class="kd"&gt;let&lt;/span&gt; &lt;span class="nx"&gt;result&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
      &lt;span class="na"&gt;added&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;onlyInLeft&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;newCart&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;items&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;oldCart&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;items&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt;
      &lt;span class="na"&gt;removed&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;onlyInLeft&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;oldCart&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;items&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;newCart&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;items&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt;
    &lt;span class="p"&gt;};&lt;/span&gt;

    &lt;span class="nx"&gt;oldCart&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;items&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;forEach&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;oi&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
      &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;ni&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;newCart&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;items&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;find&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;i&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="nx"&gt;i&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;key&lt;/span&gt; &lt;span class="o"&gt;==&lt;/span&gt; &lt;span class="nx"&gt;oi&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;key&lt;/span&gt; &lt;span class="o"&gt;&amp;amp;&amp;amp;&lt;/span&gt; &lt;span class="nx"&gt;i&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;quantity&lt;/span&gt; &lt;span class="o"&gt;!=&lt;/span&gt; &lt;span class="nx"&gt;oi&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;quantity&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
      &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="o"&gt;!&lt;/span&gt;&lt;span class="nx"&gt;ni&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;return&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
      &lt;span class="kd"&gt;let&lt;/span&gt; &lt;span class="nx"&gt;quantity&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;ni&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;quantity&lt;/span&gt; &lt;span class="o"&gt;-&lt;/span&gt; &lt;span class="nx"&gt;oi&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;quantity&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
      &lt;span class="kd"&gt;let&lt;/span&gt; &lt;span class="nx"&gt;item&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="p"&gt;...&lt;/span&gt;&lt;span class="nx"&gt;ni&lt;/span&gt; &lt;span class="p"&gt;};&lt;/span&gt;
      &lt;span class="nx"&gt;item&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;quantity&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nb"&gt;Math&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;abs&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;quantity&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
      &lt;span class="nx"&gt;quantity&lt;/span&gt; &lt;span class="o"&gt;&amp;gt;&lt;/span&gt; &lt;span class="mi"&gt;0&lt;/span&gt;
        &lt;span class="p"&gt;?&lt;/span&gt; &lt;span class="nx"&gt;result&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;added&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;push&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;item&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
        &lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;result&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;removed&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;push&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;item&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="p"&gt;});&lt;/span&gt;

    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="nx"&gt;result&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
  &lt;span class="p"&gt;}&lt;/span&gt;

  &lt;span class="k"&gt;async&lt;/span&gt; &lt;span class="nx"&gt;emitCartChanges&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;newCart&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="k"&gt;this&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;fetchCart&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;
    &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;oldCart&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;this&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;storedCart&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;
    &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;changes&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;this&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;findCartChanges&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;oldCart&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;newCart&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;

    &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;event&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nx"&gt;CustomEvent&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;cart_changed&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="na"&gt;detail&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;changes&lt;/span&gt; &lt;span class="p"&gt;});&lt;/span&gt;
    &lt;span class="nb"&gt;window&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;dispatchEvent&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;event&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;

    &lt;span class="k"&gt;this&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;storeCart&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;newCart&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
  &lt;span class="p"&gt;}&lt;/span&gt;

 &lt;span class="nx"&gt;observeCartChanges&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;cartObserver&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nx"&gt;PerformanceObserver&lt;/span&gt;&lt;span class="p"&gt;((&lt;/span&gt;&lt;span class="nx"&gt;list&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
      &lt;span class="nx"&gt;list&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;getEntries&lt;/span&gt;&lt;span class="p"&gt;().&lt;/span&gt;&lt;span class="nx"&gt;forEach&lt;/span&gt;&lt;span class="p"&gt;((&lt;/span&gt;&lt;span class="nx"&gt;entry&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;isValidRequestType&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;xmlhttprequest&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;fetch&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;].&lt;/span&gt;&lt;span class="nx"&gt;includes&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;entry&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;initiatorType&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
        &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;isCartChangeRequest&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="sr"&gt;/&lt;/span&gt;&lt;span class="se"&gt;\/&lt;/span&gt;&lt;span class="sr"&gt;cart&lt;/span&gt;&lt;span class="se"&gt;\/&lt;/span&gt;&lt;span class="sr"&gt;/&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;test&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;entry&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;name&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
        &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;isValidRequestType&lt;/span&gt; &lt;span class="o"&gt;&amp;amp;&amp;amp;&lt;/span&gt; &lt;span class="nx"&gt;isCartChangeRequest&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
          &lt;span class="k"&gt;this&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;emitCartChanges&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;
        &lt;span class="p"&gt;}&lt;/span&gt;
      &lt;span class="p"&gt;});&lt;/span&gt;
    &lt;span class="p"&gt;});&lt;/span&gt;
    &lt;span class="nx"&gt;cartObserver&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;observe&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt; &lt;span class="na"&gt;entryTypes&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;resource&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="p"&gt;});&lt;/span&gt;
  &lt;span class="p"&gt;}&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Here'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;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight javascript"&gt;&lt;code&gt;&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;myCartWatcher&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nx"&gt;CartWatcher&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="nx"&gt;myCartWatcher&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;init&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;
&lt;span class="nb"&gt;window&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;addEventListener&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;cart_changed&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;e&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="nx"&gt;console&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;log&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;e&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;detail&lt;/span&gt;&lt;span class="p"&gt;));&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&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;ol&gt;

&lt;li id="fn1"&gt;
&lt;p&gt;According to the &lt;a href="https://shopify.dev/docs/api/ajax/reference/cart"&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.&amp;nbsp;↩&lt;/p&gt;
&lt;/li&gt;

&lt;/ol&gt;

</description>
      <category>shopify</category>
      <category>javascript</category>
    </item>
    <item>
      <title>Detect subscription orders in the Shopify checkout</title>
      <dc:creator>Mike Fallows</dc:creator>
      <pubDate>Fri, 17 Feb 2023 00:00:00 +0000</pubDate>
      <link>https://dev.to/mikenewbuild/detect-subscription-orders-in-the-shopify-checkout-441f</link>
      <guid>https://dev.to/mikenewbuild/detect-subscription-orders-in-the-shopify-checkout-441f</guid>
      <description>&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 advertising. As their business model is heavily geared towards subscriptions and subscriptions will typically indicate a much higher LTV, 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's down to the limited amount of data available on the 'Thank You' page of Shopify's checkout.&lt;/p&gt;

&lt;h2&gt;
  
  
  The result
&lt;/h2&gt;

&lt;p&gt;Here's the final logic that I used in the additional scripts part of the checkout:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight ruby"&gt;&lt;code&gt;&lt;span class="c1"&gt;# track subscriptions&lt;/span&gt;
&lt;span class="n"&gt;assign&lt;/span&gt; &lt;span class="n"&gt;includes_subscription&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="kp"&gt;false&lt;/span&gt;
&lt;span class="k"&gt;for&lt;/span&gt; &lt;span class="n"&gt;line_item&lt;/span&gt; &lt;span class="k"&gt;in&lt;/span&gt; &lt;span class="n"&gt;line_items&lt;/span&gt;
  &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;line_item&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;selling_plan_allocation&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;selling_plan&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;name&lt;/span&gt; &lt;span class="n"&gt;contains&lt;/span&gt; &lt;span class="s1"&gt;'Delivery every'&lt;/span&gt;
    &lt;span class="n"&gt;assign&lt;/span&gt; &lt;span class="n"&gt;includes_subscription&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="kp"&gt;true&lt;/span&gt;
  &lt;span class="n"&gt;endif&lt;/span&gt;
&lt;span class="n"&gt;endfor&lt;/span&gt;

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

&lt;/div&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 'Subscribe' event:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight liquid"&gt;&lt;code&gt;&lt;span class="p"&gt;{%-&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="kr"&gt;if&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nv"&gt;includes_subscription&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="o"&gt;==&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="kc"&gt;true&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;%}&lt;/span&gt;
  fbq('track', 'Subscribe');
&lt;span class="p"&gt;{%-&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="kr"&gt;endif&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;-%}&lt;/span&gt;

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

&lt;/div&gt;



&lt;p&gt;To include as a &lt;code&gt;dataLayer&lt;/code&gt; variable.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight javascript"&gt;&lt;code&gt;&lt;span class="nx"&gt;dataLayer&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;push&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt;
  &lt;span class="na"&gt;event&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;purchase&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="na"&gt;ecommerce&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="c1"&gt;// ...&lt;/span&gt;
    &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;includes_subscription&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{{&lt;/span&gt; &lt;span class="nx"&gt;includes_subscription&lt;/span&gt; &lt;span class="o"&gt;|&lt;/span&gt; &lt;span class="na"&gt;default&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kc"&gt;false&lt;/span&gt; &lt;span class="p"&gt;}},&lt;/span&gt;
    &lt;span class="c1"&gt;// ...&lt;/span&gt;
  &lt;span class="p"&gt;}&lt;/span&gt;
&lt;span class="p"&gt;});&lt;/span&gt;

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

&lt;/div&gt;



&lt;h2&gt;
  
  
  Why the hacky inspection of selling_plan.name?
&lt;/h2&gt;

&lt;p&gt;As I mentioned earlier, you're a bit limited with the data you can access on the initial 'Thank You' page that you're presented with immediately after placing an order. That's because the full order hasn'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's also very hard to test the Thank You page, as to my knowledge you can'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 'Order Status' 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 'Thank You' 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's case (English language only checkout), the selling plan name for a line item would always begin 'Delivery every' as in, &lt;em&gt;'Delivery every 1 week'&lt;/em&gt;, &lt;em&gt;'Delivery every 2 weeks'&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="https://shopify.dev/docs/api/pixels/customer-events#checkout_completed"&gt;Checkout Completed Customer Event&lt;/a&gt; instead of the checkout additional scripts. And while I welcome the introduction of this feature, I'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;

</description>
      <category>shopify</category>
    </item>
    <item>
      <title>Adding search to an Eleventy site</title>
      <dc:creator>Mike Fallows</dc:creator>
      <pubDate>Wed, 08 Feb 2023 00:00:00 +0000</pubDate>
      <link>https://dev.to/mikenewbuild/adding-search-to-an-eleventy-site-133l</link>
      <guid>https://dev.to/mikenewbuild/adding-search-to-an-eleventy-site-133l</guid>
      <description>&lt;p&gt;TL;DR I went with &lt;a href="https://pagefind.app/"&gt;Pagefind&lt;/a&gt; and followed &lt;a href="https://rknight.me/using-pagefind-with-eleventy-for-search/"&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'd got my node versions out of sync, but that's to be expected.&lt;/p&gt;

&lt;h2&gt;
  
  
  Finding my search solution
&lt;/h2&gt;

&lt;p&gt;Weirdly, I'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've written because it has left my head. For a long time I'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's pretty easy to navigate to the information I'm looking for from the homepage. Still, I'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'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'm working on has already implemented it. I wasn'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'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'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="https://www.raymondcamden.com/2019/10/20/adding-search-to-your-eleventy-static-site-with-lunr"&gt;adding Lunr to an Eleventy site&lt;/a&gt; and one on &lt;a href="https://www.raymondcamden.com/2020/06/24/adding-algolia-search-to-eleventy-and-netlify"&gt;using Algolia&lt;/a&gt;. Ultimately, both those posts came with caveats, but they both seem like viable solutions. &lt;a href="https://lunrjs.com/"&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'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'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="https://mikefallows.com/search/"&gt;search page&lt;/a&gt; is now live.&lt;/p&gt;

</description>
      <category>11ty</category>
      <category>eleventy</category>
    </item>
    <item>
      <title>Hello World with Lit</title>
      <dc:creator>Mike Fallows</dc:creator>
      <pubDate>Mon, 24 Oct 2022 00:00:00 +0000</pubDate>
      <link>https://dev.to/mikenewbuild/hello-world-with-lit-54dd</link>
      <guid>https://dev.to/mikenewbuild/hello-world-with-lit-54dd</guid>
      <description>&lt;p&gt;The world of Web Components seems to be getting more attention recently so I've been exploring some of the frameworks emerging around it. Here are some notes on my initial experience with &lt;a href="https://lit.dev/"&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's pretty much 90% of what I'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="https://lit.dev/tutorials/"&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't sure if Lit required a build step so I decided I'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="https://www.youtube.com/watch?v=QBa1_QQnRcs&amp;amp;t=302s"&gt;introductory screencast&lt;/a&gt; I found on YouTube used TypeScript and I'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's marketing itself as an alternative to developers coming from the React ecosystem and that seems perfectly reasonable. I'm personally coming from a "sprinkles"&lt;sup id="fnref1"&gt;1&lt;/sup&gt; 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;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight html"&gt;&lt;code&gt;&lt;span class="cp"&gt;&amp;lt;!DOCTYPE html&amp;gt;&lt;/span&gt;
&lt;span class="nt"&gt;&amp;lt;html&lt;/span&gt; &lt;span class="na"&gt;lang=&lt;/span&gt;&lt;span class="s"&gt;"en"&lt;/span&gt;&lt;span class="nt"&gt;&amp;gt;&lt;/span&gt;
&lt;span class="nt"&gt;&amp;lt;head&amp;gt;&lt;/span&gt;
  &lt;span class="nt"&gt;&amp;lt;meta&lt;/span&gt; &lt;span class="na"&gt;charset=&lt;/span&gt;&lt;span class="s"&gt;"utf-8"&lt;/span&gt;&lt;span class="nt"&gt;&amp;gt;&lt;/span&gt;
  &lt;span class="nt"&gt;&amp;lt;meta&lt;/span&gt; &lt;span class="na"&gt;name=&lt;/span&gt;&lt;span class="s"&gt;"viewport"&lt;/span&gt; &lt;span class="na"&gt;content=&lt;/span&gt;&lt;span class="s"&gt;"width=device-width, initial-scale=1.0"&lt;/span&gt;&lt;span class="nt"&gt;&amp;gt;&lt;/span&gt;
  &lt;span class="nt"&gt;&amp;lt;title&amp;gt;&lt;/span&gt;Lit Test&lt;span class="nt"&gt;&amp;lt;/title&amp;gt;&lt;/span&gt;
  &lt;span class="nt"&gt;&amp;lt;script &lt;/span&gt;&lt;span class="na"&gt;type=&lt;/span&gt;&lt;span class="s"&gt;"module"&lt;/span&gt;&lt;span class="nt"&gt;&amp;gt;&lt;/span&gt;
    &lt;span class="k"&gt;import&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;html&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;LitElement&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="k"&gt;from&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;https://cdn.jsdelivr.net/gh/lit/dist@2/core/lit-core.min.js&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;

    &lt;span class="kd"&gt;class&lt;/span&gt; &lt;span class="nx"&gt;HelloWorld&lt;/span&gt; &lt;span class="kd"&gt;extends&lt;/span&gt; &lt;span class="nx"&gt;LitElement&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
      &lt;span class="nx"&gt;render&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="nx"&gt;html&lt;/span&gt;&lt;span class="s2"&gt;`&amp;lt;p&amp;gt;Hello world&amp;lt;/p&amp;gt;`&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
      &lt;span class="p"&gt;}&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt;

    &lt;span class="nx"&gt;customElements&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;define&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;hello-world&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;HelloWorld&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
  &lt;span class="nt"&gt;&amp;lt;/script&amp;gt;&lt;/span&gt;
&lt;span class="nt"&gt;&amp;lt;/head&amp;gt;&lt;/span&gt;
&lt;span class="nt"&gt;&amp;lt;body&amp;gt;&lt;/span&gt;
  &lt;span class="nt"&gt;&amp;lt;hello-world&amp;gt;&amp;lt;/hello-world&amp;gt;&lt;/span&gt;
&lt;span class="nt"&gt;&amp;lt;/body&amp;gt;&lt;/span&gt;
&lt;span class="nt"&gt;&amp;lt;/html&amp;gt;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&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="https://cdn.jsdelivr.net/gh/lit/dist@2.4.0/core/"&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="https://www.digitalocean.com/community/tutorials/js-tagged-template-literals"&gt;tagged template literals&lt;/a&gt;. For comparison, here's the boilerplate you would need to write the same functionality without Lit.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight javascript"&gt;&lt;code&gt;&lt;span class="kd"&gt;class&lt;/span&gt; &lt;span class="nx"&gt;HelloWorld&lt;/span&gt; &lt;span class="kd"&gt;extends&lt;/span&gt; &lt;span class="nx"&gt;HTMLElement&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="kd"&gt;constructor&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="k"&gt;super&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;
    &lt;span class="k"&gt;this&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;attachShadow&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt; &lt;span class="na"&gt;mode&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;open&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt; &lt;span class="p"&gt;});&lt;/span&gt;
    &lt;span class="kd"&gt;let&lt;/span&gt; &lt;span class="nx"&gt;p&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nb"&gt;document&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;createElement&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;p&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
    &lt;span class="nx"&gt;p&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;innerText&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;Hello world&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
    &lt;span class="k"&gt;this&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;shadowRoot&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;append&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;p&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
  &lt;span class="p"&gt;}&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;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'm unfamiliar with the syntax and I wasn'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;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight javascript"&gt;&lt;code&gt;&lt;span class="kd"&gt;class&lt;/span&gt; &lt;span class="nx"&gt;HelloWorld&lt;/span&gt; &lt;span class="kd"&gt;extends&lt;/span&gt; &lt;span class="nx"&gt;LitElement&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="kd"&gt;static&lt;/span&gt; &lt;span class="nx"&gt;properties&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="na"&gt;name&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="na"&gt;type&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;String&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt;
  &lt;span class="p"&gt;}&lt;/span&gt;
  &lt;span class="nx"&gt;render&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="nx"&gt;html&lt;/span&gt;&lt;span class="s2"&gt;`&amp;lt;p&amp;gt;Hello &lt;/span&gt;&lt;span class="p"&gt;${&lt;/span&gt;&lt;span class="k"&gt;this&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;name&lt;/span&gt; &lt;span class="o"&gt;||&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;world&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;&amp;lt;/p&amp;gt;`&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
  &lt;span class="p"&gt;}&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Allows you to do:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight html"&gt;&lt;code&gt;&lt;span class="c"&gt;&amp;lt;!-- output: Hello world --&amp;gt;&lt;/span&gt;
&lt;span class="nt"&gt;&amp;lt;hello-world&amp;gt;&amp;lt;/hello-world&amp;gt;&lt;/span&gt;
&lt;span class="c"&gt;&amp;lt;!-- output: Hello you --&amp;gt;&lt;/span&gt;
&lt;span class="nt"&gt;&amp;lt;hello-world&lt;/span&gt; &lt;span class="na"&gt;name=&lt;/span&gt;&lt;span class="s"&gt;"you"&lt;/span&gt;&lt;span class="nt"&gt;&amp;gt;&amp;lt;/hello-world&amp;gt;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h2&gt;
  
  
  Mutating the data
&lt;/h2&gt;

&lt;p&gt;That's all good, but how about adding some behaviour to the component? This is where Lit'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'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;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight javascript"&gt;&lt;code&gt;&lt;span class="kd"&gt;class&lt;/span&gt; &lt;span class="nx"&gt;HelloWorld&lt;/span&gt; &lt;span class="kd"&gt;extends&lt;/span&gt; &lt;span class="nx"&gt;LitElement&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="kd"&gt;static&lt;/span&gt; &lt;span class="nx"&gt;properties&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="na"&gt;name&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="na"&gt;type&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;String&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt;
  &lt;span class="p"&gt;}&lt;/span&gt;
  &lt;span class="kd"&gt;constructor&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="k"&gt;super&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;
    &lt;span class="k"&gt;this&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;name&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;world&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
  &lt;span class="p"&gt;}&lt;/span&gt;
  &lt;span class="nx"&gt;makeLouder&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="k"&gt;this&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;name&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;this&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;name&lt;/span&gt; &lt;span class="o"&gt;+&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;!&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;
  &lt;span class="p"&gt;}&lt;/span&gt;
  &lt;span class="nx"&gt;render&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="nx"&gt;html&lt;/span&gt;&lt;span class="s2"&gt;`
    &amp;lt;p&amp;gt;Hello &lt;/span&gt;&lt;span class="p"&gt;${&lt;/span&gt;&lt;span class="k"&gt;this&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;name&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;&amp;lt;/p&amp;gt;
    &amp;lt;button @click=&lt;/span&gt;&lt;span class="p"&gt;${&lt;/span&gt;&lt;span class="k"&gt;this&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;makeLouder&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;&amp;gt;Louder&amp;lt;/button&amp;gt;
    `&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
  &lt;span class="p"&gt;}&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;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 a 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'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;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight javascript"&gt;&lt;code&gt;&lt;span class="k"&gt;import&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;css&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;html&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;LitElement&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="k"&gt;from&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;https://cdn.jsdelivr.net/gh/lit/dist@2/core/lit-core.min.js&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;

&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;fontFamily&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;css&lt;/span&gt;&lt;span class="s2"&gt;`sans-serif`&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

&lt;span class="kd"&gt;class&lt;/span&gt; &lt;span class="nx"&gt;HelloWorld&lt;/span&gt; &lt;span class="kd"&gt;extends&lt;/span&gt; &lt;span class="nx"&gt;LitElement&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="kd"&gt;static&lt;/span&gt; &lt;span class="nx"&gt;styles&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;css&lt;/span&gt;&lt;span class="s2"&gt;`
    :host {
      display: block;
      padding: 1rem;
      background: var(--background, #eee);
    }
    :host(.blue) {
      background: aliceblue;
    }
    p {
      font-family: &lt;/span&gt;&lt;span class="p"&gt;${&lt;/span&gt;&lt;span class="nx"&gt;fontFamily&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;;
    }
  `&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
  &lt;span class="nx"&gt;render&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="nx"&gt;html&lt;/span&gt;&lt;span class="s2"&gt;`&amp;lt;p&amp;gt;Hello world&amp;lt;/p&amp;gt;`&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
  &lt;span class="p"&gt;}&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;





&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight html"&gt;&lt;code&gt;&lt;span class="c"&gt;&amp;lt;!-- Background #eee --&amp;gt;&lt;/span&gt;
&lt;span class="nt"&gt;&amp;lt;hello-world&amp;gt;&amp;lt;/hello-world&amp;gt;&lt;/span&gt;
&lt;span class="c"&gt;&amp;lt;!-- Background aliceblue --&amp;gt;&lt;/span&gt;
&lt;span class="nt"&gt;&amp;lt;hello-world&lt;/span&gt; &lt;span class="na"&gt;class=&lt;/span&gt;&lt;span class="s"&gt;"blue"&lt;/span&gt;&lt;span class="nt"&gt;&amp;gt;&amp;lt;/hello-world&amp;gt;&lt;/span&gt;
&lt;span class="c"&gt;&amp;lt;!-- Background goldenrod --&amp;gt;&lt;/span&gt;
&lt;span class="nt"&gt;&amp;lt;style&amp;gt;&lt;/span&gt;
&lt;span class="nt"&gt;hello-world&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="py"&gt;--background&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="no"&gt;goldenrod&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;span class="nt"&gt;&amp;lt;/style&amp;gt;&lt;/span&gt;
&lt;span class="nt"&gt;&amp;lt;hello-world&amp;gt;&amp;lt;/hello-world&amp;gt;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&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'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;ol&gt;

&lt;li id="fn1"&gt;
&lt;p&gt;The JavaScript "sprinkles" 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.&amp;nbsp;↩&lt;/p&gt;
&lt;/li&gt;

&lt;/ol&gt;

</description>
      <category>javascript</category>
      <category>learninpublic</category>
      <category>webcomponents</category>
    </item>
    <item>
      <title>Adding a `robots.txt` file to an Eleventy site</title>
      <dc:creator>Mike Fallows</dc:creator>
      <pubDate>Thu, 01 Sep 2022 00:00:00 +0000</pubDate>
      <link>https://dev.to/mikenewbuild/adding-a-robotstxt-file-to-an-eleventy-site-1e34</link>
      <guid>https://dev.to/mikenewbuild/adding-a-robotstxt-file-to-an-eleventy-site-1e34</guid>
      <description>&lt;p&gt;Having set up a fair few sites over the years, I periodically get a chunk of emails from Google'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'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't have expected. The errors I had were for:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;https://mikefallows.com/cdn-cgi/l/email-protection
https://mikefallows.com/admin/
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&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;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight html"&gt;&lt;code&gt;&lt;span class="nt"&gt;&amp;lt;meta&lt;/span&gt; &lt;span class="na"&gt;name=&lt;/span&gt;&lt;span class="s"&gt;"robots"&lt;/span&gt; &lt;span class="na"&gt;content=&lt;/span&gt;&lt;span class="s"&gt;"noindex"&lt;/span&gt; &lt;span class="nt"&gt;/&amp;gt;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&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's being included in my &lt;code&gt;sitemap.xml&lt;/code&gt; so it'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'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'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'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;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight javascript"&gt;&lt;code&gt;&lt;span class="c1"&gt;// Put robots.txt in root&lt;/span&gt;
&lt;span class="nx"&gt;eleventyConfig&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;addPassthroughCopy&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;src/robots.txt&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;/robots.txt&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt; &lt;span class="p"&gt;});&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The &lt;code&gt;addPassthroughCopy&lt;/code&gt; method will just copy the file "as is" 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;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&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;/div&gt;



&lt;p&gt;The important parts were the two &lt;code&gt;Disallow&lt;/code&gt; rules that tell bots that they shouldn't try to crawl or index those paths in their results.&lt;/p&gt;

&lt;p&gt;You can also &lt;a href="https://mikefallows.com/robots.txt"&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;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight html"&gt;&lt;code&gt;---
permalink: /sitemap.xml
eleventyExcludeFromCollections: true
---
&lt;span class="cp"&gt;&amp;lt;?xml version="1.0" encoding="utf-8"?&amp;gt;&lt;/span&gt;
&lt;span class="nt"&gt;&amp;lt;urlset&lt;/span&gt; &lt;span class="na"&gt;xmlns=&lt;/span&gt;&lt;span class="s"&gt;"http://www.sitemaps.org/schemas/sitemap/0.9"&lt;/span&gt;&lt;span class="nt"&gt;&amp;gt;&lt;/span&gt;
{%- for page in collections.all %}
  {%- set adminUrl = r/^\/admin\//i.test(page.url) %}
  {%- set draft = page.data.draft %}
  {%- if not adminUrl and not draft %}
    {%- set absoluteUrl %}{{ page.url | url | absoluteUrl(metadata.url) }}{% endset %}
    &lt;span class="nt"&gt;&amp;lt;url&amp;gt;&lt;/span&gt;
      &lt;span class="nt"&gt;&amp;lt;loc&amp;gt;&lt;/span&gt;{{ absoluteUrl }}&lt;span class="nt"&gt;&amp;lt;/loc&amp;gt;&lt;/span&gt;
      &lt;span class="nt"&gt;&amp;lt;lastmod&amp;gt;&lt;/span&gt;{{ page.date | htmlDateString }}&lt;span class="nt"&gt;&amp;lt;/lastmod&amp;gt;&lt;/span&gt;
      &lt;span class="nt"&gt;&amp;lt;changefreq&amp;gt;&lt;/span&gt;{{ page.data.changeFreq if page.data.changeFreq else "monthly" }}&lt;span class="nt"&gt;&amp;lt;/changefreq&amp;gt;&lt;/span&gt;
    &lt;span class="nt"&gt;&amp;lt;/url&amp;gt;&lt;/span&gt;
  {%- endif %}
{%- endfor %}
&lt;span class="nt"&gt;&amp;lt;/urlset&amp;gt;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;This generates &lt;a href="https://mikefallows.com/sitemap.xml"&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'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;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight ruby"&gt;&lt;code&gt;&lt;span class="n"&gt;set&lt;/span&gt; &lt;span class="n"&gt;adminUrl&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;r&lt;/span&gt;&lt;span class="o"&gt;/^&lt;/span&gt;&lt;span class="p"&gt;\&lt;/span&gt;&lt;span class="sr"&gt;/admin\//i&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;test&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;page&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;url&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&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're only matching the start of the string&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;\/admin\/&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'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're low pressure like this one, it'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's mostly due to taking the time to write it up.&lt;/p&gt;

</description>
      <category>11ty</category>
      <category>eleventy</category>
      <category>seo</category>
    </item>
    <item>
      <title>Laravel Sail, Vite and SSL with a custom domain</title>
      <dc:creator>Mike Fallows</dc:creator>
      <pubDate>Fri, 29 Jul 2022 00:00:00 +0000</pubDate>
      <link>https://dev.to/mikenewbuild/laravel-sail-vite-and-ssl-with-a-custom-domain-4jg6</link>
      <guid>https://dev.to/mikenewbuild/laravel-sail-vite-and-ssl-with-a-custom-domain-4jg6</guid>
      <description>&lt;p&gt;The Shopify apps I build are typically powered by Laravel. Version 8 introduced &lt;a href="https://laravel.com/docs/master/sail"&gt;Sail&lt;/a&gt;&lt;sup id="fnref1"&gt;1&lt;/sup&gt; as an alternative to Valet and version 9 introduced &lt;a href="https://vitejs.dev/"&gt;Vite&lt;/a&gt;&lt;sup id="fnref2"&gt;2&lt;/sup&gt; 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;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="nv"&gt;APP_URL&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="s2"&gt;"http://myproject.test"&lt;/span&gt;
&lt;span class="nv"&gt;APP_SERVICE&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="s2"&gt;"myproject.test"&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&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;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight yaml"&gt;&lt;code&gt;&lt;span class="na"&gt;version&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s1"&gt;'&lt;/span&gt;&lt;span class="s"&gt;3'&lt;/span&gt;
&lt;span class="na"&gt;services&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
  &lt;span class="na"&gt;myproject.test&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
    &lt;span class="na"&gt;build&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
    &lt;span class="c1"&gt;# etc...&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&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="https://gilbitron.me/blog/enabling-https-ssl-for-laravel-sail-using-caddy/"&gt;this guide by Gilbert Pellegrom&lt;/a&gt; to enable HTTPS for Sail using Caddy. I won'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's post kindly &lt;a href="https://gist.github.com/gilbitron/36d48eb751875bebdf43da0a91c9faec"&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;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="nv"&gt;APP_URL&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="s2"&gt;"https://myproject.test"&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;I also found I needed to add a line to the &lt;code&gt;AppServiceProvider&lt;/code&gt;'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;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight php"&gt;&lt;code&gt;&lt;span class="k"&gt;public&lt;/span&gt; &lt;span class="k"&gt;function&lt;/span&gt; &lt;span class="n"&gt;boot&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
&lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="err"&gt;\&lt;/span&gt;&lt;span class="no"&gt;Illuminate\Support\Facades\URL&lt;/span&gt;&lt;span class="o"&gt;::&lt;/span&gt;&lt;span class="nf"&gt;forceScheme&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s1"&gt;'https'&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;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't want the warnings to keep appearing and you'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's guide.&lt;/p&gt;

&lt;h2&gt;
  
  
  Securing Vite'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't load the styles and scripts on my pages. It's possible to set Vite to load its server under HTTPS, but it won't have a certificate. You can use the &lt;a href="https://github.com/vitejs/vite-plugin-basic-ssl"&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="https://stackoverflow.com/a/73148121/15761479"&gt;Stack Overflow answer&lt;/a&gt; to set your Vite config'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'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="https://github.com/liuweiGL/vite-plugin-mkcert"&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'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'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;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight javascript"&gt;&lt;code&gt;&lt;span class="k"&gt;import&lt;/span&gt; &lt;span class="nx"&gt;laravel&lt;/span&gt; &lt;span class="k"&gt;from&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;laravel-vite-plugin&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;
&lt;span class="k"&gt;import&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;defineConfig&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="k"&gt;from&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;vite&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;
&lt;span class="k"&gt;import&lt;/span&gt; &lt;span class="nx"&gt;mkcert&lt;/span&gt; &lt;span class="k"&gt;from&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;vite-plugin-mkcert&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;

&lt;span class="k"&gt;export&lt;/span&gt; &lt;span class="k"&gt;default&lt;/span&gt; &lt;span class="nx"&gt;defineConfig&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt;
    &lt;span class="na"&gt;server&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
      &lt;span class="na"&gt;https&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kc"&gt;true&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
      &lt;span class="na"&gt;host&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;vite.myproject.test&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="p"&gt;},&lt;/span&gt;
    &lt;span class="na"&gt;plugins&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;
        &lt;span class="nx"&gt;mkcert&lt;/span&gt;&lt;span class="p"&gt;(),&lt;/span&gt;
        &lt;span class="nx"&gt;laravel&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt;
          &lt;span class="na"&gt;input&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;
            &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;resources/css/app.css&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
            &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;resources/js/app.js&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
          &lt;span class="p"&gt;],&lt;/span&gt;
          &lt;span class="na"&gt;refresh&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kc"&gt;true&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="p"&gt;}),&lt;/span&gt;
    &lt;span class="p"&gt;],&lt;/span&gt;
&lt;span class="p"&gt;})&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;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;ol&gt;

&lt;li id="fn1"&gt;
&lt;p&gt;"A light-weight command-line interface for interacting with Laravel's default Docker development environment". Recently I've preferred to use Docker over Laravel Valet as I'm regularly switching between projects with different PHP versions and database engines, as well as working locally on either Intel or Silicon hardware. &amp;nbsp;↩&lt;/p&gt;
&lt;/li&gt;

&lt;li id="fn2"&gt;
&lt;p&gt;"A modern frontend build tool that provides an extremely fast development environment". I'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.&amp;nbsp;↩&lt;/p&gt;
&lt;/li&gt;

&lt;/ol&gt;

</description>
      <category>laravel</category>
      <category>shopify</category>
      <category>vite</category>
    </item>
    <item>
      <title>Bulk deleting Cloudflare DNS records</title>
      <dc:creator>Mike Fallows</dc:creator>
      <pubDate>Mon, 25 Jul 2022 00:00:00 +0000</pubDate>
      <link>https://dev.to/mikenewbuild/bulk-deleting-cloudflare-dns-records-f5m</link>
      <guid>https://dev.to/mikenewbuild/bulk-deleting-cloudflare-dns-records-f5m</guid>
      <description>&lt;p&gt;I'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't own, but happen to manage on behalf of clients. I've long been wanting to tinker with Cloudflare as it offers a chunk of nice features like &lt;a href="https://en.wikipedia.org/wiki/Denial-of-service_attack"&gt;DDoS&lt;/a&gt; protection, asset compression, HTTPS and email routing, all on a pretty generous free plan. So far I've found it easy to work with, and after migrating a dozen or so domains to it, I've had a pretty good opportunity to try it out.&lt;/p&gt;

&lt;p&gt;When you set up a new 'site' 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've had to manually add some records myself, usually to do with email and third-party verification (&lt;a href="https://en.wikipedia.org/wiki/DMARC"&gt;DMARC&lt;/a&gt;, &lt;a href="https://en.wikipedia.org/wiki/Sender_Policy_Framework"&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'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="https://en.wikipedia.org/wiki/Domain_Name_System"&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;del&gt;wasted some time&lt;/del&gt; &lt;em&gt;did the sensible thing&lt;/em&gt; and &lt;a href="https://github.com/mikenewbuild/cloudflare"&gt;put together a little repo&lt;/a&gt; that could be configured. I haven't done much PHP recently so it was also an excuse to flex that muscle. It's still my favourite scripting language for putting something together quickly.&lt;/p&gt;

&lt;p&gt;I honestly couldn't figure out why this happened on some domains and not others, there wasn't anything I could identify as the root cause. Maybe you're here because you've encountered the same problem? If so hopefully &lt;a href="https://github.com/mikenewbuild/cloudflare"&gt;this script might help you out&lt;/a&gt;!&lt;/p&gt;

</description>
      <category>cloudflare</category>
      <category>dns</category>
    </item>
    <item>
      <title>Making a Shopify Theme App Extension for Google Site Verification</title>
      <dc:creator>Mike Fallows</dc:creator>
      <pubDate>Wed, 23 Feb 2022 00:00:00 +0000</pubDate>
      <link>https://dev.to/mikenewbuild/making-a-shopify-theme-app-extension-for-google-site-verification-5860</link>
      <guid>https://dev.to/mikenewbuild/making-a-shopify-theme-app-extension-for-google-site-verification-5860</guid>
      <description>&lt;p&gt;Given that I'm &lt;em&gt;all in&lt;/em&gt; on Shopify's OS2 themes, I've been looking for a way to experiment with building the recently introduced "app extensions" 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="https://shopify.dev/apps/tools/cli"&gt;Shopify CLI&lt;/a&gt; installed, then it'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'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'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;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight html"&gt;&lt;code&gt;&lt;span class="nt"&gt;&amp;lt;meta&lt;/span&gt; &lt;span class="na"&gt;name=&lt;/span&gt;&lt;span class="s"&gt;"google-site-verification"&lt;/span&gt; &lt;span class="na"&gt;content=&lt;/span&gt;&lt;span class="s"&gt;"UNIQUE_ID"&lt;/span&gt;&lt;span class="nt"&gt;&amp;gt;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;I want to be able to have a theme block that allows adding the client's &lt;code&gt;UNIQUE_ID&lt;/code&gt; as a setting in the theme'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't have one set up already. The easiest way is to use the CLI'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's not particularly important because I'm not worried about creating any UI for the app right now.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;shopify app create node
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Next, I supplied a name for the app. I just picked something generic: 'Theme Blocks'. It will also ask you which type of app you want to create, for simplicity I picked 'Custom' 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="https://shopify.dev/apps/online-store/theme-app-extensions/extensions-framework"&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's &lt;a href="https://shopify.dev/apps/online-store/theme-app-extensions/getting-started"&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't actually that explicit in the documentation so, at first, I wasn't sure if it should be treated as a separate project in its own repo. Don't make the same mistake. At the root of the project I ran:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;shopify extension create
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&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;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight liquid"&gt;&lt;code&gt;&lt;span class="p"&gt;{%-&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="kr"&gt;if&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nv"&gt;block&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nv"&gt;settings&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nv"&gt;google_verification_id&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="o"&gt;!=&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nb"&gt;blank&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;%}&lt;/span&gt;
&amp;lt;meta name="google-site-verification" content="&lt;span class="p"&gt;{{&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nv"&gt;block&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nv"&gt;settings&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nv"&gt;google_verification_id&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;}}&lt;/span&gt;"&amp;gt;
&lt;span class="p"&gt;{%-&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="kr"&gt;endif&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;%}&lt;/span&gt;

&lt;span class="p"&gt;{%&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nt"&gt;schema&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;%}&lt;/span&gt;
{
  "name": "Theme Customisations",
  "target": "head",
  "settings": [
    {
      "type": "text",
      "id": "google_verification_id",
      "label": "Google verification ID"
    }
  ]
}
&lt;span class="p"&gt;{%&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nt"&gt;endschema&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;%}&lt;/span&gt;

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

&lt;/div&gt;



&lt;p&gt;This should make sense if you're familiar with Shopify's theme sections. Essentially I'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;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;shopify extension push
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&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 'draft' 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;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;shopify extension connect
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&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'm not sure if there'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'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;

</description>
      <category>shopify</category>
    </item>
  </channel>
</rss>
