<?xml version="1.0" encoding="utf-8"?>
<feed xmlns="http://www.w3.org/2005/Atom">
  <title>Alistair Shepherd</title>
  <subtitle></subtitle>
  <link href="https://www.alistairshepherd.uk/feed.xml" rel="self"/>
  <link href="https://www.alistairshepherd.uk/"/>
  <updated>2026-03-25T00:00:00Z</updated>
  <id>https://www.alistairshepherd.uk</id>
  <author>
    <name></name>
  </author>
  
    
    <entry>
      <title>Fixes for Koken Problems</title>
      <link href="https://www.alistairshepherd.uk/writing/fixes-for-koken-problems/"/>
      <updated>2020-09-17T00:00:00Z</updated>
      <id>https://www.alistairshepherd.uk/writing/fixes-for-koken-problems/</id>
      <content type="html">
        <![CDATA[
      <p><a href="http://koken.me/">Koken</a> was a self-hosted Content Management System built for photographers, offering off-the-shelf features like Adobe Lightroom integration, EXIF handling, album categorisation and much more.</p>
<p>5 years ago, if you were looking for a free photo management website you could host yourself, Koken was a great option. I started with Koken for a photography client in 2014, and decided upon it based on the great documentation and very easy theme development.</p>
<p>Note: I would highly suggest against anyone setting up a new Koken site now. I'm not even sure if it's possible! This is for people maintaining their existing sites.</p>
<p><img
	src="/images/monolith-koken-theme.jpg?width=763"
	srcset="/images/monolith-koken-theme.jpg?width=400 400w, /images/monolith-koken-theme.jpg?width=600 600w, /images/monolith-koken-theme.jpg?width=800 800w, /images/monolith-koken-theme.jpg?width=1077 1077w, /images/monolith-koken-theme.jpg?width=1500 1500w, /images/monolith-koken-theme.jpg?width=2154 2154w"
	sizes="(min-width: 50rem) 763px, calc(100vw - 2.66rem)"
	width="763"
	height="458"
	alt="Screenshot of a website. The headers says Monolith - Koken Theme by Accudio"
	loading="lazy"></p>
<h2>History of Koken</h2>
<p>Koken's public beta was released in Early 2013, and got fairly popular pretty quick. It's creator <a href="https://www.todddominey.com/">Todd Dominey</a> nailed what photographers wanted and needed from a self-hosted CMS. Although it became stable and fairly well-supported, it never made it to 1.0.</p>
<p>Fast forward a couple years and in 2015 Koken is <a href="http://blog.koken.me/essays/2015/10/netobjects-acquires-koken/">bought by NetObjects</a>, a software company which in the 90s had success with a desktop site-builder.</p>
<p>NetObjects continued to update Koken for around 2 years  (although focusing on premium functionality), with the last release of v0.22.24 in August 2017. As far as I am aware, there has been no further development of Koken since then. The help centre and social media was active for about another year before they also had no further updates.</p>
<p>As of 2020 Koken still functions, but has some major issues. The store and documentation went offline sometime around the end of 2019 which obviously makes development harder and prevents the easy installation of themes, plugins, and even causes issues with logins on some older versions.</p>
<p>There was a Community Koken Forum called Koken Community, but as Koken died it went not long after, understandably given the lack of any official support.</p>
<h2>My relationship with Koken</h2>
<p>Every few weeks I get a message on twitter or an email from someone asking for help with their Koken site. This is because I have the 'honour' of being the last tweet on Koken's <a href="https://twitter.com/koken">twitter @koken</a>. In 2017 I developed a theme for Koken for the previously mentioned client called Monolith. Over the end of 2017 and early 2018 I refined this and released it onto GitHub open-sourced under a GPL-v3.0 license. At this point I was unaware of the problems going on with Koken and perhaps had I known that client would be on a different platform.</p>
<p>Since 2017 I have been maintaining a Koken site, and have encountered a few problems that I am consistently asked about. These are my fixes for them, I hope they help!</p>
<p>Note these are all made on v0.22.24 and may differ for previous versions.</p>
<h2>&quot;Cannot connect to the API&quot; on login, PHP 7+</h2>
<p>When using Koken with PHP 7+ (confirmed on PHP 7.3 and 7.4) and visiting /admin/, you may get a red error box appear with &quot;Cannot connect to the API&quot; without any further error message. Usually this appears with a database error etc, but this is code related. To fix it:</p>
<p>In your installation, find <code>/app/database/DB_Driver.php</code> and take a look at line 1018 and you should see something like this:</p>
<pre class="language-php"><code class="language-php"><span class="token keyword">else</span><br><span class="token punctuation">{</span><br>	<span class="token variable">$args</span> <span class="token operator">=</span> <span class="token punctuation">(</span><span class="token function">func_num_args</span><span class="token punctuation">(</span><span class="token punctuation">)</span> <span class="token operator">></span> <span class="token number">1</span><span class="token punctuation">)</span> <span class="token operator">?</span> <span class="token function">array_splice</span><span class="token punctuation">(</span><span class="token function">func_get_args</span><span class="token punctuation">(</span><span class="token punctuation">)</span><span class="token punctuation">,</span> <span class="token number">1</span><span class="token punctuation">)</span> <span class="token punctuation">:</span> <span class="token class-name return-type">null</span><span class="token punctuation">;</span><br>	<span class="token keyword">if</span> <span class="token punctuation">(</span><span class="token function">is_null</span><span class="token punctuation">(</span><span class="token variable">$args</span><span class="token punctuation">)</span><span class="token punctuation">)</span><br>	<span class="token punctuation">{</span><br>		<span class="token keyword">return</span> <span class="token function">call_user_func</span><span class="token punctuation">(</span><span class="token variable">$function</span><span class="token punctuation">)</span><span class="token punctuation">;</span><br>	<span class="token punctuation">}</span><br>	<span class="token keyword">else</span><br>	<span class="token punctuation">{</span><br>		<span class="token keyword">return</span> <span class="token function">call_user_func_array</span><span class="token punctuation">(</span><span class="token variable">$function</span><span class="token punctuation">,</span> <span class="token variable">$args</span><span class="token punctuation">)</span><span class="token punctuation">;</span><br>	<span class="token punctuation">}</span><br><span class="token punctuation">}</span></code></pre>
<p>Now, replace line 1028 (the $args declaration) with the following two lines:</p>
<pre class="language-php"><code class="language-php"><span class="token variable">$func_args</span> <span class="token operator">=</span> <span class="token function">func_get_args</span><span class="token punctuation">(</span><span class="token punctuation">)</span><span class="token punctuation">;</span><br><span class="token variable">$args</span> <span class="token operator">=</span> <span class="token punctuation">(</span><span class="token function">func_num_args</span><span class="token punctuation">(</span><span class="token punctuation">)</span> <span class="token operator">></span> <span class="token number">1</span><span class="token punctuation">)</span> <span class="token operator">?</span> <span class="token function">array_splice</span><span class="token punctuation">(</span><span class="token variable">$func_args</span><span class="token punctuation">,</span> <span class="token number">1</span><span class="token punctuation">)</span> <span class="token punctuation">:</span> <span class="token class-name return-type">null</span><span class="token punctuation">;</span></code></pre>
<p>The code should now look like:</p>
<pre class="language-php"><code class="language-php"><span class="token keyword">else</span><br><span class="token punctuation">{</span><br>	<span class="token variable">$func_args</span> <span class="token operator">=</span> <span class="token function">func_get_args</span><span class="token punctuation">(</span><span class="token punctuation">)</span><span class="token punctuation">;</span><br>	<span class="token variable">$args</span> <span class="token operator">=</span> <span class="token punctuation">(</span><span class="token function">func_num_args</span><span class="token punctuation">(</span><span class="token punctuation">)</span> <span class="token operator">></span> <span class="token number">1</span><span class="token punctuation">)</span> <span class="token operator">?</span> <span class="token function">array_splice</span><span class="token punctuation">(</span><span class="token variable">$func_args</span><span class="token punctuation">,</span> <span class="token number">1</span><span class="token punctuation">)</span> <span class="token punctuation">:</span> <span class="token class-name return-type">null</span><span class="token punctuation">;</span><br>	<span class="token keyword">if</span> <span class="token punctuation">(</span><span class="token function">is_null</span><span class="token punctuation">(</span><span class="token variable">$args</span><span class="token punctuation">)</span><span class="token punctuation">)</span><br>	<span class="token punctuation">{</span><br>		<span class="token keyword">return</span> <span class="token function">call_user_func</span><span class="token punctuation">(</span><span class="token variable">$function</span><span class="token punctuation">)</span><span class="token punctuation">;</span><br>	<span class="token punctuation">}</span><br>	<span class="token keyword">else</span><br>	<span class="token punctuation">{</span><br>		<span class="token keyword">return</span> <span class="token function">call_user_func_array</span><span class="token punctuation">(</span><span class="token variable">$function</span><span class="token punctuation">,</span> <span class="token variable">$args</span><span class="token punctuation">)</span><span class="token punctuation">;</span><br>	<span class="token punctuation">}</span><br><span class="token punctuation">}</span></code></pre>
<h2>Images not loading, PHP 7+</h2>
<p>You may not notice this immediately if you have images cached, but if you cleared the cache or uploaded a new image you may find it is not rendered. This can also occur in PHP 7+ (confirmed with 7.3 and 7.4) and is another easy fix.</p>
<p>Find <code>/i.php</code> in the root of your installation. On lines 13 and 14 there is the following:</p>
<pre class="language-php"><code class="language-php"><span class="token keyword">require</span> <span class="token variable">$root</span> <span class="token operator">.</span> <span class="token string single-quoted-string">'/app/koken/Shutter/Shutter.php'</span><span class="token punctuation">;</span><br><span class="token keyword">require</span> <span class="token variable">$root</span> <span class="token operator">.</span> <span class="token string single-quoted-string">'/app/koken/Utils/KokenAPI.php'</span><span class="token punctuation">;</span></code></pre>
<p>Replace those lines with:</p>
<pre class="language-php"><code class="language-php"><span class="token keyword">require_once</span> <span class="token variable">$root</span> <span class="token operator">.</span> <span class="token string single-quoted-string">'/app/koken/Shutter/Shutter.php'</span><span class="token punctuation">;</span><br><span class="token keyword">require_once</span> <span class="token variable">$root</span> <span class="token operator">.</span> <span class="token string single-quoted-string">'/app/koken/Utils/KokenAPI.php'</span><span class="token punctuation">;</span></code></pre>
<p>You also need to open <code>/app/koken/Shutter/Shutter.php</code> and on line 274 replace the following:</p>
<pre class="language-php"><code class="language-php"><span class="token keyword">include</span> <span class="token function">dirname</span><span class="token punctuation">(</span><span class="token constant">__DIR__</span><span class="token punctuation">)</span> <span class="token operator">.</span> <span class="token string single-quoted-string">'/Utils/KokenAPI.php'</span><span class="token punctuation">;</span></code></pre>
<p>with:</p>
<pre class="language-php"><code class="language-php"><span class="token keyword">include_once</span> <span class="token function">dirname</span><span class="token punctuation">(</span><span class="token constant">__DIR__</span><span class="token punctuation">)</span> <span class="token operator">.</span> <span class="token string single-quoted-string">'/Utils/KokenAPI.php'</span><span class="token punctuation">;</span></code></pre>
<h2>Forgotten Password</h2>
<p>First try to enter a wrong password and click the &quot;Forgot Password&quot; link that appears in the bottom right. This is the easiest way on more recent version of Koken</p>
<p>Unfortunately however it seems that previous versions relied upon <a href="http://store.koken.me">store.koken.me</a> in order to offer forgotten password functionality. We can still reset the password, but it's a bit more manual.</p>
<p>You need access to your koken database for this, whether than be through phpmyadmin, another database management tool, or mysql on the command line. I won't bore with the exact commands/clicks required for each, just the general process.</p>
<ol>
<li>Gain access to the database. If you don't know the login details, you can find them in <code>/storage/configuration/database.php</code>;</li>
<li>Go to the <code>koken_users</code> table. This should have only one entry, your user with associated email etc;</li>
<li>Copy the <code>internal_id</code> for the user;</li>
<li>Then go to &quot;<a href="http://yourwebsite.com/api.php?/users/reset_password/">http://yourwebsite.com/api.php?/users/reset_password/</a>&quot; with the id at the end of the URL;</li>
<li>Visiting that link should send the email attached to your account a new password</li>
</ol>
<h2>Conclusion</h2>
<p>If you haven't gathered from the rest of this post, unfortunately Koken is dead. I will continue to maintain a Koken site as my client cannot afford the cost of a rebuild and relies upon the Lightroom integration, something I haven't seen anywhere else. I would however say you shouldn't be setting up any new Koken websites, and if you still have one you should be seriously looking at alternatives.</p>
<p>There is a <a href="https://www.change.org/p/netobjects-relinquish-koken-to-opensource">change.org petition</a> calling on NetObjects to open-source Koken but I am not hopeful. They are a commercial software company, and most software companies will cling to thier code to the end.</p>
<p>A couple of people have asked me about alternatives now, so I've included a list below. Unfortunately there's nothing quite like it Koken but hopefully one might fill your needs</p>
<p>If you have any further issues with Koken then feel free to leave a comment and I'd be happy to help.</p>
<h2>Koken Alternatives</h2>
<h3>Custom-built website</h3>
<p>As a freelance developer I am more or less obligated to suggest that if you have the money, the best solution you will get will be through a web developer or development agency. This can be the design you want and function exactly as you need. If you're interested in my services as a developer or want some throughts on what you need, feel free to get in touch.</p>
<h3>Hosted Photography Solutions</h3>
<p>There are hosted photography portfolio services out there including:</p>
<ul>
<li><a href="https://www.pixpa.com/">Pixpa</a></li>
<li><a href="https://www.photoshelter.com/">Photoshelter</a></li>
<li><a href="https://www.format.com/">Format</a></li>
<li><a href="https://www.clickbooq.com/">Clickbooq</a></li>
</ul>
<p>I haven't used any of these platforms but I have heard they are reliable and easy-to-use and not too expensive.</p>
<h3>Hosted Websites</h3>
<p>For a bit more work you could use a hosted website builder like <a href="https://www.squarespace.com/">Squarespace</a>, <a href="https://www.wix.com/">Wix</a>, or <a href="https://wordpress.com/">WordPress.com</a>. This will give you a bit more flexibility than a platform designed to fill a niche.</p>
<h2>WordPress</h2>
<p>As much as <a href="https://wordpress.org/">WordPress</a> can have a bad reputation, if you consider themes and plugins carefully then you can get a great looking and performing WordPress site for photography.</p>
<p>It's worth looking for themes that are designed for photography so functionality like EXIF data, lightboxes, and copy protection is included. I don't have any examples but there are several free ones on the official theme directory and many options in commercial theme directories like ThemeForest.</p>
<h2>For more Technical...</h2>
<p>If you're fairly technically minded, there are also some great programs designed to generate a static website from your content and once you get used to them are really quick and simple.</p>
<p>One I've come across is called &quot;Prosopopee&quot; (<a href="https://github.com/Psycojoker/prosopopee/">github.com/Psycojoker/prosopopee/</a>) and is designed for photography websites and features everything you'd need. It's a bit more involved to publish content as it's done with text files rather than a GUI or Lightroom integration, and you'll probably initially need a developer to make it look how you'd like but that wouldn't be too hard to come across.</p>
<p>Not focused on Photography, but again for the more technical 11ty (<a href="https://11ty.dev">11ty.dev</a>) is a static site generator that could absolutely work as a brilliant image gallery. You'd also probably need a developer to get the initial site going and perhaps integrate it with a headless CMS, but with a bit of extra work you can get a system that's a lot more flexible and resilient.</p>
<p>If I were to set up a new site for a photographer, this is probably the direction I'd go down. I've been burned by Koken dying in only a few years, a static site and static site generator will be around a lot longer than that.</p>

      <hr>
      <p>If you have any comments or feedback on this article, let me know! I'd love to hear your thoughts, go ahead and <a href="mailto:alistair@accudio.com">send me an email</a> at alistair@accudio.com or <a href="https://front-end.social/@accudio" rel="noopener noreferrer">contact me on Mastodon</a>.</p>
      <p>The post <a href="https://www.alistairshepherd.uk/writing/fixes-for-koken-problems/">Fixes for Koken Problems</a> appeared first on <a href="https://alistairshepherd.uk">alistairshepherd.uk</a>.</p>
    ]]>
      </content>
    </entry>
  
    
    <entry>
      <title>Making a Parallax SVG Landscape - new site part 1</title>
      <link href="https://www.alistairshepherd.uk/writing/parallax-svg-landscape-1/"/>
      <updated>2021-01-29T00:00:00Z</updated>
      <id>https://www.alistairshepherd.uk/writing/parallax-svg-landscape-1/</id>
      <content type="html">
        <![CDATA[
      <p>I've finally finished my new website, and I am really happy with it. I started the site in July 2020 and I launched it in mid January 2021.</p>
<p>It's a big change from Nuxt, Vue and Webpack to doing pretty much everything myself with 11ty (Eleventy) and gulp—I love it. More on that in a future post however, today is about the star of the show—the parallax landscape you see at the top of the page.</p>
<p>If you're the type who wants to dive straight into the code, <a href="https://codepen.io/accudio/pen/ExNxXrP">here's a CodePen</a> - go and have a play!</p>
<iframe class="codepen" height="380" style="width:100%;" scrolling="no" title="CodePen Embed" src="https://codepen.io/anon/embed/ExNxXrP?height=380&theme-id=dark&default-tab=result" frameborder="0" loading="lazy" allowtransparency="true" allowfullscreen="true"><p><a href="https://codepen.io/accudio/pen/ExNxXrP" target="_blank" rel="noopener">See the Pen</a></p></iframe>
<p>For those still with me, let's go through it.</p>
<p><em>Note: I'm writing JavaScript in ES6 and CSS in SCSS. I compile my code anyway so this makes it easier for me to work with.</em></p>
<h2>Inspiration</h2>
<p>If you recognise the art-style, it's <strong>super</strong> inspired by  the game <a href="https://firewatchgame.com">Firewatch</a>. Firewatch is a 'walking simulator' game that came out in 2016 and people loved its art style. Featuring a bright, layered landscape it inspired many, myself included. For several years the wallpaper of my phone changed between <a href="https://imgur.com/a/snB5O">these wallpapers</a> based on time and weather.</p>
<p>When I was planning my new site, I decided to centre it on this art style. I wanted it to feel interactive, and parallax felt like a natural way to do that.</p>
<h2>The Markup</h2>
<p>My wonderful sister <a href="https://beccishep.co.uk">Becci Shepherd</a> produced the landscape, and sent me a raster PNG for each layer. Although I experimented with masking, it's browser support isn't quite there. SVGs were the obvious choice.</p>
<p>To convert to vector I used <a href="https://vectormagic.com/desktop">Vector Magic Desktop Edition</a>. It does a brilliant job of anything you throw at it, and is the best raster-to-vector converter I've found.</p>
<p>I tidied up the paths in a graphics program; exported it to SVG; tidied up the markup and optimised with <a href="https://jakearchibald.github.io/svgomg/">SVGOMG</a>. This left me with a decent sized SVG for each layer.</p>
<p><em>Try ensure the viewbox is identical as it will make sizing much easier.</em></p>
<p>Now in HTML, we need to stack them:</p>
<pre class="language-html"><code class="language-html"><span class="token tag"><span class="token tag"><span class="token punctuation">&lt;</span>div</span> <span class="token attr-name">class</span><span class="token attr-value"><span class="token punctuation attr-equals">=</span><span class="token punctuation">"</span>landscape<span class="token punctuation">"</span></span> <span class="token attr-name">role</span><span class="token attr-value"><span class="token punctuation attr-equals">=</span><span class="token punctuation">"</span>img<span class="token punctuation">"</span></span> <span class="token attr-name">aria-label</span><span class="token attr-value"><span class="token punctuation attr-equals">=</span><span class="token punctuation">"</span>This is equivalent to an img alt attribute.<span class="token punctuation">"</span></span><span class="token punctuation">></span></span><br>	<span class="token tag"><span class="token tag"><span class="token punctuation">&lt;</span>div</span> <span class="token attr-name">class</span><span class="token attr-value"><span class="token punctuation attr-equals">=</span><span class="token punctuation">"</span>landscape__layer<span class="token punctuation">"</span></span><span class="token punctuation">></span></span><br>    	<span class="token tag"><span class="token tag"><span class="token punctuation">&lt;</span>div</span> <span class="token attr-name">class</span><span class="token attr-value"><span class="token punctuation attr-equals">=</span><span class="token punctuation">"</span>landscape__image<span class="token punctuation">"</span></span><span class="token punctuation">></span></span><br>      		<span class="token tag"><span class="token tag"><span class="token punctuation">&lt;</span>svg</span> <span class="token attr-name">viewBox</span><span class="token attr-value"><span class="token punctuation attr-equals">=</span><span class="token punctuation">"</span>0 0 4000 1000<span class="token punctuation">"</span></span> <span class="token attr-name">xmlns</span><span class="token attr-value"><span class="token punctuation attr-equals">=</span><span class="token punctuation">"</span>http://www.w3.org/2000/svg<span class="token punctuation">"</span></span><span class="token punctuation">></span></span>...<span class="token tag"><span class="token tag"><span class="token punctuation">&lt;/</span>svg</span><span class="token punctuation">></span></span><br>    	<span class="token tag"><span class="token tag"><span class="token punctuation">&lt;/</span>div</span><span class="token punctuation">></span></span><br>  	<span class="token tag"><span class="token tag"><span class="token punctuation">&lt;/</span>div</span><span class="token punctuation">></span></span><br>	<span class="token tag"><span class="token tag"><span class="token punctuation">&lt;</span>div</span> <span class="token attr-name">class</span><span class="token attr-value"><span class="token punctuation attr-equals">=</span><span class="token punctuation">"</span>landscape__layer<span class="token punctuation">"</span></span><span class="token punctuation">></span></span><br>    	<span class="token tag"><span class="token tag"><span class="token punctuation">&lt;</span>div</span> <span class="token attr-name">class</span><span class="token attr-value"><span class="token punctuation attr-equals">=</span><span class="token punctuation">"</span>landscape__image<span class="token punctuation">"</span></span><span class="token punctuation">></span></span><br>      		<span class="token tag"><span class="token tag"><span class="token punctuation">&lt;</span>svg</span> <span class="token attr-name">viewBox</span><span class="token attr-value"><span class="token punctuation attr-equals">=</span><span class="token punctuation">"</span>0 0 4000 1000<span class="token punctuation">"</span></span> <span class="token attr-name">xmlns</span><span class="token attr-value"><span class="token punctuation attr-equals">=</span><span class="token punctuation">"</span>http://www.w3.org/2000/svg<span class="token punctuation">"</span></span><span class="token punctuation">></span></span>...<span class="token tag"><span class="token tag"><span class="token punctuation">&lt;/</span>svg</span><span class="token punctuation">></span></span><br>    	<span class="token tag"><span class="token tag"><span class="token punctuation">&lt;/</span>div</span><span class="token punctuation">></span></span><br>  	<span class="token tag"><span class="token tag"><span class="token punctuation">&lt;/</span>div</span><span class="token punctuation">></span></span><br>	<span class="token tag"><span class="token tag"><span class="token punctuation">&lt;</span>div</span> <span class="token attr-name">class</span><span class="token attr-value"><span class="token punctuation attr-equals">=</span><span class="token punctuation">"</span>landscape__layer<span class="token punctuation">"</span></span><span class="token punctuation">></span></span><br>    	<span class="token tag"><span class="token tag"><span class="token punctuation">&lt;</span>div</span> <span class="token attr-name">class</span><span class="token attr-value"><span class="token punctuation attr-equals">=</span><span class="token punctuation">"</span>landscape__image<span class="token punctuation">"</span></span><span class="token punctuation">></span></span><br>      		<span class="token tag"><span class="token tag"><span class="token punctuation">&lt;</span>svg</span> <span class="token attr-name">viewBox</span><span class="token attr-value"><span class="token punctuation attr-equals">=</span><span class="token punctuation">"</span>0 0 4000 1000<span class="token punctuation">"</span></span> <span class="token attr-name">xmlns</span><span class="token attr-value"><span class="token punctuation attr-equals">=</span><span class="token punctuation">"</span>http://www.w3.org/2000/svg<span class="token punctuation">"</span></span><span class="token punctuation">></span></span>...<span class="token tag"><span class="token tag"><span class="token punctuation">&lt;/</span>svg</span><span class="token punctuation">></span></span><br>    	<span class="token tag"><span class="token tag"><span class="token punctuation">&lt;/</span>div</span><span class="token punctuation">></span></span><br>  	<span class="token tag"><span class="token tag"><span class="token punctuation">&lt;/</span>div</span><span class="token punctuation">></span></span><br><br>  	... and so on.<br><span class="token tag"><span class="token tag"><span class="token punctuation">&lt;/</span>div</span><span class="token punctuation">></span></span></code></pre>
<p><em>Remember accessibility! Despite being a whole bunch of markup, this is really a fancy image. We use</em> <code>role=&quot;img&quot;</code> <em>and</em> <code>aria-label</code> <em>to make it accessible.</em></p>
<p>I didn't have the two wrapping <code>div</code>s at first, but realised that wrappers for each layer allowed me to use flexbox. This made positioning the SVGs easier:</p>
<pre class="language-scss"><code class="language-scss"><span class="token comment">// wrapping landscape</span><br><span class="token selector">.landscape </span><span class="token punctuation">{</span><br>	<span class="token property">background</span><span class="token punctuation">:</span> <span class="token function">var</span><span class="token punctuation">(</span>--c1<span class="token punctuation">)</span><span class="token punctuation">;</span><br>	<span class="token property">height</span><span class="token punctuation">:</span> 75vh<span class="token punctuation">;</span><br>	<span class="token property">overflow</span><span class="token punctuation">:</span> hidden<span class="token punctuation">;</span><br>	<span class="token property">position</span><span class="token punctuation">:</span> relative<span class="token punctuation">;</span><br><br><span class="token comment">// make each layer fill parent</span><br><span class="token selector">.landscape__layer </span><span class="token punctuation">{</span><br>	<span class="token property">height</span><span class="token punctuation">:</span> 100%<span class="token punctuation">;</span><br>	<span class="token property">left</span><span class="token punctuation">:</span> 0<span class="token punctuation">;</span><br>	<span class="token property">position</span><span class="token punctuation">:</span> absolute<span class="token punctuation">;</span><br>	<span class="token property">top</span><span class="token punctuation">:</span> 0<span class="token punctuation">;</span><br>	<span class="token property">width</span><span class="token punctuation">:</span> 100%<span class="token punctuation">;</span><br><span class="token punctuation">}</span><br><br><span class="token comment">// svg wrapper</span><br><span class="token selector">.landscape__image </span><span class="token punctuation">{</span><br>	<span class="token comment">// position at bottom of element in center</span><br>	<span class="token property">position</span><span class="token punctuation">:</span> absolute<span class="token punctuation">;</span><br>	<span class="token property">bottom</span><span class="token punctuation">:</span> 0<span class="token punctuation">;</span><br>	<span class="token property">left</span><span class="token punctuation">:</span> 50%<span class="token punctuation">;</span><br>	<span class="token property">transform</span><span class="token punctuation">:</span> <span class="token function">translateX</span><span class="token punctuation">(</span>-50%<span class="token punctuation">)</span><span class="token punctuation">;</span><br><br>	<span class="token comment">// set sizes that work for my image</span><br>	<span class="token property">max-height</span><span class="token punctuation">:</span> 100%<span class="token punctuation">;</span><br>	<span class="token property">max-width</span><span class="token punctuation">:</span> 300%<span class="token punctuation">;</span><br>	<span class="token property">min-width</span><span class="token punctuation">:</span> 100%<span class="token punctuation">;</span><br>	<span class="token property">width</span><span class="token punctuation">:</span> 2500px<span class="token punctuation">;</span><br><br>	<span class="token comment">// use flexbox to center SVG elements</span><br>	<span class="token property">display</span><span class="token punctuation">:</span> flex<span class="token punctuation">;</span><br>	<span class="token property">flex-direction</span><span class="token punctuation">:</span> column<span class="token punctuation">;</span><br><span class="token punctuation">}</span><br><br><span class="token comment">// basic styling for SVG element</span><br><span class="token selector">.landscape__image svg </span><span class="token punctuation">{</span><br>	<span class="token property">display</span><span class="token punctuation">:</span> block<span class="token punctuation">;</span><br>	<span class="token property">height</span><span class="token punctuation">:</span> auto<span class="token punctuation">;</span><br>	<span class="token property">max-width</span><span class="token punctuation">:</span> 100%<span class="token punctuation">;</span><br><span class="token punctuation">}</span></code></pre>
<p>We now have a static landscape and are set up to make it more dynamic!</p>
<iframe class="codepen" height="380" style="width:100%;" scrolling="no" title="CodePen Embed" src="https://codepen.io/anon/embed/eYBYEWz?height=380&theme-id=dark&default-tab=result" frameborder="0" loading="lazy" allowtransparency="true" allowfullscreen="true"><p><a href="https://codepen.io/accudio/pen/eYBYEWz" target="_blank" rel="noopener">See the Pen</a></p></iframe>
<h2>Parallax Animation</h2>
<h3>Perspective and translateZ</h3>
<p>There are two popular methods to implement parallax on the web. The more performant implementation is a CSS-only solution using the <code>perspective</code> CSS property with <code>translateZ()</code>. This is what browser vendors suggest, as it allows the browser to render changes with the GPU. This makes it super quick and smooth and is how I tried to implement it for weeks.</p>
<p>Google Developer docs have a <a href="https://developers.google.com/web/updates/2016/12/performant-parallaxing">good example of this method</a>.</p>
<p>Although it's great for simple implementations—I found that in my case it was unreliable. This was because:</p>
<ul>
<li>Browser implementations vary. Chrome handles a deep 3D perspective easily, but Firefox interprets the spec differently. This meant I had to apply <code>transform-style: preserve-3d</code> on every element between my scroll element and my layers.</li>
<li>Firefox on Android handles 3D transformations inconsistently with deep DOM trees, making it hard to adapt to my structure.</li>
<li>Mobile Safari on iOS has quirks, and the current solution is to use a trick which 'reverses' the parallax direction. Not an option in my case as that would break the effect!</li>
</ul>
<p>I spent about two weeks trying to get this working before giving up and going for method two.</p>
<h3>JavaScript parallax</h3>
<p>JS-based parallax has had a bad rep, as a few popular libraries weren't very performant or accessible. Their size was to deal with browser inconsistencies, but with modern CSS and JS we can do it ourselves without much work.</p>
<p>With CSS custom properties and <code>calc()</code> we can come up with a light and neat implementation ourselves. In JavaScript we use <code>window.requestAnimationFrame</code> and if the scroll position has changed we set it to a custom property.</p>
<pre class="language-js"><code class="language-js"><span class="token comment">// constant elements: your main scrolling element; html element</span><br><span class="token keyword">const</span> scrollEl <span class="token operator">=</span> document<span class="token punctuation">.</span>documentElement<br><span class="token keyword">const</span> root <span class="token operator">=</span> document<span class="token punctuation">.</span>documentElement<br><br><span class="token keyword">let</span> scrollPos<br><br><span class="token comment">// update css property on scroll</span><br><span class="token keyword">function</span> <span class="token function">animation</span><span class="token punctuation">(</span><span class="token punctuation">)</span> <span class="token punctuation">{</span><br>  <span class="token comment">// check the scroll position has changed</span><br>  <span class="token keyword">if</span> <span class="token punctuation">(</span>scrollPos <span class="token operator">!==</span> scrollEl<span class="token punctuation">.</span>scrollTop<span class="token punctuation">)</span> <span class="token punctuation">{</span><br>    <span class="token comment">// reset the seen scroll position</span><br>    scrollPos <span class="token operator">=</span> scrollEl<span class="token punctuation">.</span>scrollTop<br>    <span class="token comment">// update css property --scrollPos with scroll position in pixels</span><br>    root<span class="token punctuation">.</span>style<span class="token punctuation">.</span><span class="token function">setProperty</span><span class="token punctuation">(</span><span class="token string">'--scrollPos'</span><span class="token punctuation">,</span> scrollPos <span class="token operator">+</span> <span class="token string">'px'</span><span class="token punctuation">)</span><br>  <span class="token punctuation">}</span><br><br>  <span class="token comment">// call animation again on next animation frame</span><br>  window<span class="token punctuation">.</span><span class="token function">requestAnimationFrame</span><span class="token punctuation">(</span>animation<span class="token punctuation">)</span><br><span class="token punctuation">}</span><br><br><span class="token comment">// start animation on next animation frame</span><br>window<span class="token punctuation">.</span><span class="token function">requestAnimationFrame</span><span class="token punctuation">(</span>animation<span class="token punctuation">)</span></code></pre>
<p>That's it. That's all the JavaScript we need. As someone who <strong>loves CSS</strong> it feels great knowing that we can keep the JS simple and use CSS to implement this <em>descriptively</em>.</p>
<p>The real action is happening in the CSS, this is what we need to add to our previous styles:</p>
<pre class="language-scss"><code class="language-scss"><span class="token selector">.landscape__layer </span><span class="token punctuation">{</span><br>	<span class="token comment">// parallax</span><br>	<span class="token property">transform</span><span class="token punctuation">:</span> <span class="token function">translateY</span><span class="token punctuation">(</span><span class="token function">calc</span><span class="token punctuation">(</span><span class="token function">var</span><span class="token punctuation">(</span>--scrollPos<span class="token punctuation">,</span> 0<span class="token punctuation">)</span> <span class="token operator">*</span> <span class="token function">var</span><span class="token punctuation">(</span>--offset<span class="token punctuation">,</span> 0<span class="token punctuation">)</span><span class="token punctuation">)</span><span class="token punctuation">)</span><span class="token punctuation">;</span><br><br>	<span class="token atrule"><span class="token rule">@media</span> <span class="token punctuation">(</span><span class="token property">prefers-reduced-motion</span><span class="token punctuation">:</span> reduce<span class="token punctuation">)</span></span> <span class="token punctuation">{</span><br>		<span class="token property">transform</span><span class="token punctuation">:</span> <span class="token function">translateY</span><span class="token punctuation">(</span>0<span class="token punctuation">)</span><span class="token punctuation">;</span><br>	<span class="token punctuation">}</span><br><span class="token punctuation">}</span></code></pre>
<p>The key line is the first <code>transform</code> and it's custom properties. What we are doing is translating the layer down a certain amount based on the scroll position.</p>
<p>We use a <code>prefers-reduced-motion</code> media query to remove the parallax effect for those who might get motion-sick or prefer less movement in their browsing.</p>
<p>The <code>--offset</code> property is a value that would be between 0 and 1, and changes how much that layer scrolls. Let's look at what happens when we vary that property and scroll down by <code>100px</code>:</p>
<ul>
<li><code>--offset: 0</code> — the element isn't translated and scrolls as normal;</li>
<li><code>--offset: 0.5</code> — the element will be translated down by <code>50px</code>. This makes it look like it's moved <code>50px</code>;</li>
<li><code>--offset: 1</code> — the element is translated down <code>100px</code>, it's in the same place it used to be. This makes it look like it's not moving with scroll;</li>
</ul>
<p>The <code>--offset</code> property is the key to our parallax system. If each layer has a different value it will scroll at a different speed from the other layers. We can manually set how much each layer will scroll so it looks natural.</p>
<p>The way we apply this to our layers is using the style property. This way we can avoid adding any more CSS, no matter how many layers we have. We set the front layer to 0 so it scrolls with the content, and increase it with each layer. This is what worked for my image:</p>
<pre class="language-html"><code class="language-html"><span class="token tag"><span class="token tag"><span class="token punctuation">&lt;</span>div</span> <span class="token attr-name">class</span><span class="token attr-value"><span class="token punctuation attr-equals">=</span><span class="token punctuation">"</span>landscape<span class="token punctuation">"</span></span> <span class="token attr-name">role</span><span class="token attr-value"><span class="token punctuation attr-equals">=</span><span class="token punctuation">"</span>img<span class="token punctuation">"</span></span> <span class="token attr-name">aria-label</span><span class="token attr-value"><span class="token punctuation attr-equals">=</span><span class="token punctuation">"</span>This is equivalent to an img alt attribute.<span class="token punctuation">"</span></span><span class="token punctuation">></span></span><br>	<span class="token tag"><span class="token tag"><span class="token punctuation">&lt;</span>div</span> <span class="token attr-name">class</span><span class="token attr-value"><span class="token punctuation attr-equals">=</span><span class="token punctuation">"</span>landscape__layer<span class="token punctuation">"</span></span> <span class="token special-attr"><span class="token attr-name">style</span><span class="token attr-value"><span class="token punctuation attr-equals">=</span><span class="token punctuation">"</span><span class="token value css language-css"><span class="token property">--offset</span><span class="token punctuation">:</span>0.96</span><span class="token punctuation">"</span></span></span><span class="token punctuation">></span></span>...<span class="token tag"><span class="token tag"><span class="token punctuation">&lt;/</span>div</span><span class="token punctuation">></span></span><br>	<span class="token tag"><span class="token tag"><span class="token punctuation">&lt;</span>div</span> <span class="token attr-name">class</span><span class="token attr-value"><span class="token punctuation attr-equals">=</span><span class="token punctuation">"</span>landscape__layer<span class="token punctuation">"</span></span> <span class="token special-attr"><span class="token attr-name">style</span><span class="token attr-value"><span class="token punctuation attr-equals">=</span><span class="token punctuation">"</span><span class="token value css language-css"><span class="token property">--offset</span><span class="token punctuation">:</span>0.92</span><span class="token punctuation">"</span></span></span><span class="token punctuation">></span></span>...<span class="token tag"><span class="token tag"><span class="token punctuation">&lt;/</span>div</span><span class="token punctuation">></span></span><br>	<span class="token tag"><span class="token tag"><span class="token punctuation">&lt;</span>div</span> <span class="token attr-name">class</span><span class="token attr-value"><span class="token punctuation attr-equals">=</span><span class="token punctuation">"</span>landscape__layer<span class="token punctuation">"</span></span> <span class="token special-attr"><span class="token attr-name">style</span><span class="token attr-value"><span class="token punctuation attr-equals">=</span><span class="token punctuation">"</span><span class="token value css language-css"><span class="token property">--offset</span><span class="token punctuation">:</span>0.9</span><span class="token punctuation">"</span></span></span><span class="token punctuation">></span></span>...<span class="token tag"><span class="token tag"><span class="token punctuation">&lt;/</span>div</span><span class="token punctuation">></span></span><br>	<span class="token tag"><span class="token tag"><span class="token punctuation">&lt;</span>div</span> <span class="token attr-name">class</span><span class="token attr-value"><span class="token punctuation attr-equals">=</span><span class="token punctuation">"</span>landscape__layer<span class="token punctuation">"</span></span> <span class="token special-attr"><span class="token attr-name">style</span><span class="token attr-value"><span class="token punctuation attr-equals">=</span><span class="token punctuation">"</span><span class="token value css language-css"><span class="token property">--offset</span><span class="token punctuation">:</span>0.86</span><span class="token punctuation">"</span></span></span><span class="token punctuation">></span></span>...<span class="token tag"><span class="token tag"><span class="token punctuation">&lt;/</span>div</span><span class="token punctuation">></span></span><br>	<span class="token tag"><span class="token tag"><span class="token punctuation">&lt;</span>div</span> <span class="token attr-name">class</span><span class="token attr-value"><span class="token punctuation attr-equals">=</span><span class="token punctuation">"</span>landscape__layer<span class="token punctuation">"</span></span> <span class="token special-attr"><span class="token attr-name">style</span><span class="token attr-value"><span class="token punctuation attr-equals">=</span><span class="token punctuation">"</span><span class="token value css language-css"><span class="token property">--offset</span><span class="token punctuation">:</span>0.83</span><span class="token punctuation">"</span></span></span><span class="token punctuation">></span></span>...<span class="token tag"><span class="token tag"><span class="token punctuation">&lt;/</span>div</span><span class="token punctuation">></span></span><br>	<span class="token tag"><span class="token tag"><span class="token punctuation">&lt;</span>div</span> <span class="token attr-name">class</span><span class="token attr-value"><span class="token punctuation attr-equals">=</span><span class="token punctuation">"</span>landscape__layer<span class="token punctuation">"</span></span> <span class="token special-attr"><span class="token attr-name">style</span><span class="token attr-value"><span class="token punctuation attr-equals">=</span><span class="token punctuation">"</span><span class="token value css language-css"><span class="token property">--offset</span><span class="token punctuation">:</span>0.8</span><span class="token punctuation">"</span></span></span><span class="token punctuation">></span></span>...<span class="token tag"><span class="token tag"><span class="token punctuation">&lt;/</span>div</span><span class="token punctuation">></span></span><br>	<span class="token tag"><span class="token tag"><span class="token punctuation">&lt;</span>div</span> <span class="token attr-name">class</span><span class="token attr-value"><span class="token punctuation attr-equals">=</span><span class="token punctuation">"</span>landscape__layer<span class="token punctuation">"</span></span> <span class="token special-attr"><span class="token attr-name">style</span><span class="token attr-value"><span class="token punctuation attr-equals">=</span><span class="token punctuation">"</span><span class="token value css language-css"><span class="token property">--offset</span><span class="token punctuation">:</span>0.75</span><span class="token punctuation">"</span></span></span><span class="token punctuation">></span></span>...<span class="token tag"><span class="token tag"><span class="token punctuation">&lt;/</span>div</span><span class="token punctuation">></span></span><br>	<span class="token tag"><span class="token tag"><span class="token punctuation">&lt;</span>div</span> <span class="token attr-name">class</span><span class="token attr-value"><span class="token punctuation attr-equals">=</span><span class="token punctuation">"</span>landscape__layer<span class="token punctuation">"</span></span> <span class="token special-attr"><span class="token attr-name">style</span><span class="token attr-value"><span class="token punctuation attr-equals">=</span><span class="token punctuation">"</span><span class="token value css language-css"><span class="token property">--offset</span><span class="token punctuation">:</span>0.4</span><span class="token punctuation">"</span></span></span><span class="token punctuation">></span></span>...<span class="token tag"><span class="token tag"><span class="token punctuation">&lt;/</span>div</span><span class="token punctuation">></span></span><br>	<span class="token tag"><span class="token tag"><span class="token punctuation">&lt;</span>div</span> <span class="token attr-name">class</span><span class="token attr-value"><span class="token punctuation attr-equals">=</span><span class="token punctuation">"</span>landscape__layer<span class="token punctuation">"</span></span> <span class="token special-attr"><span class="token attr-name">style</span><span class="token attr-value"><span class="token punctuation attr-equals">=</span><span class="token punctuation">"</span><span class="token value css language-css"><span class="token property">--offset</span><span class="token punctuation">:</span>0.2</span><span class="token punctuation">"</span></span></span><span class="token punctuation">></span></span>...<span class="token tag"><span class="token tag"><span class="token punctuation">&lt;/</span>div</span><span class="token punctuation">></span></span><br>	<span class="token tag"><span class="token tag"><span class="token punctuation">&lt;</span>div</span> <span class="token attr-name">class</span><span class="token attr-value"><span class="token punctuation attr-equals">=</span><span class="token punctuation">"</span>landscape__layer<span class="token punctuation">"</span></span> <span class="token special-attr"><span class="token attr-name">style</span><span class="token attr-value"><span class="token punctuation attr-equals">=</span><span class="token punctuation">"</span><span class="token value css language-css"><span class="token property">--offset</span><span class="token punctuation">:</span>0</span><span class="token punctuation">"</span></span></span><span class="token punctuation">></span></span>...<span class="token tag"><span class="token tag"><span class="token punctuation">&lt;/</span>div</span><span class="token punctuation">></span></span><br><span class="token tag"><span class="token tag"><span class="token punctuation">&lt;/</span>div</span><span class="token punctuation">></span></span></code></pre>
<p>Notice the big gap between 0.4 and 0.75. If you look at the landscape structure, the loch is a lot further away than the trees. We produce the same effect by making the offset a lot further away from 0.</p>
<h2>Result</h2>
<p>And here we have our final parallax landscape!</p>
<iframe class="codepen" height="380" style="width:100%;" scrolling="no" title="CodePen Embed" src="https://codepen.io/anon/embed/ExNxXrP?height=380&theme-id=dark&default-tab=result" frameborder="0" loading="lazy" allowtransparency="true" allowfullscreen="true"><p><a href="https://codepen.io/accudio/pen/ExNxXrP" target="_blank" rel="noopener">See the Pen</a></p></iframe>
<p>Thank you for reading! Next up we're going to take this landscape and add colour schemes—including one that matches the visitors local time!</p>

      <hr>
      <p>If you have any comments or feedback on this article, let me know! I'd love to hear your thoughts, go ahead and <a href="mailto:alistair@accudio.com">send me an email</a> at alistair@accudio.com or <a href="https://front-end.social/@accudio" rel="noopener noreferrer">contact me on Mastodon</a>.</p>
      <p>The post <a href="https://www.alistairshepherd.uk/writing/parallax-svg-landscape-1/">Making a Parallax SVG Landscape - new site part 1</a> appeared first on <a href="https://alistairshepherd.uk">alistairshepherd.uk</a>.</p>
    ]]>
      </content>
    </entry>
  
    
    <entry>
      <title>SVG Landscape with live colour theming - new site part 2</title>
      <link href="https://www.alistairshepherd.uk/writing/parallax-svg-landscape-2/"/>
      <updated>2021-02-17T00:00:00Z</updated>
      <id>https://www.alistairshepherd.uk/writing/parallax-svg-landscape-2/</id>
      <content type="html">
        <![CDATA[
      <p>As promised, now we're going to dig into the colour-changing effects of my new website. If you haven't seen it, check out my previous post about the <a href="/writing/parallax-svg-landscape-1/">SVG parallax effect in the landscape</a>.</p>
<p>If you haven't tried it yet, visit my website and click the &quot;paint bucket&quot; icon in the top-right of my website to see the theme picker. Here you can change the colour scheme of the website.</p>
<p>There are four 'static' colour schemes of 'Sunrise', 'Day', 'Sunset' and 'Night'. These set the colours to a specific palette.</p>
<p>I implemented two special 'dynamic' colour schemes, the default of 'Live' and 'Cycle'. Live sets the colour scheme of the website to roughly match your local time, whilst Cycle is a 60 second loop animating through the four static schemes above.</p>
<p>The main point of this post is the colour changing functionality, but I'll briefly mention the 'Sun' animation too.</p>
<p>If you want straight at the code, enjoy! 👋</p>
<iframe class="codepen" height="380" style="width:100%;" scrolling="no" title="CodePen Embed" src="https://codepen.io/anon/embed/GRNmbjJ?height=380&theme-id=dark&default-tab=result" frameborder="0" loading="lazy" allowtransparency="true" allowfullscreen="true"><p><a href="https://codepen.io/accudio/pen/GRNmbjJ" target="_blank" rel="noopener">See the Pen</a></p></iframe>
<p><em>Note: This post is more technical and less visual than my previous one. There aren't many demos, and it's mostly code snippets from here on. You've been warned!</em></p>
<h2>Background</h2>
<p>I have wanted to implement a 'live' functionality in my personal website for a few years. Something that makes my site feel more current and that evolves with the day excited me.</p>
<p>My first attempt at this was in my previous site, where I had a background video of a stream on the Isle of Skye. This was a simple 30s loop, but what I wanted was a 24-hour video that would be synced up with your local time. I liked this idea, but it was impractical thanks to the difficulty in getting 24 hours of consistent footage. It also turned out to be a pretty major technical challenge, I had no experience of streaming video and <a href="https://developer.mozilla.org/en-US/docs/Web/Guide/Audio_and_video_delivery/Live_streaming_web_audio_and_video#hls">HLS</a> and <a href="https://developer.mozilla.org/en-US/docs/Web/Guide/Audio_and_video_delivery/Live_streaming_web_audio_and_video#mpeg-dash">DASH</a> weren't widely supported.</p>
<p>When I came up with the idea of the SVG landscape, this seemed like a perfect accompaniment. I could make the time in the 'scene' match up with your local time and demonstrate that through the colours and sun.</p>
<p>Initially I implemented a prototype of this with <a href="https://animejs.com">anime.js</a>—a great JS animation library. When I boiled down the essential elements however, the problem was a lot simpler than I thought. There's more JavaScript here than my previous post but stick with me!</p>
<h2>Static Custom Properties</h2>
<p>We are starting from the final CodePen in my previous post. First let us set up our colours in custom properties:</p>
<iframe class="codepen" height="380" style="width:100%;" scrolling="no" title="CodePen Embed" src="https://codepen.io/anon/embed/JjbNQXw?height=380&theme-id=dark&default-tab=result" frameborder="0" loading="lazy" allowtransparency="true" allowfullscreen="true"><p><a href="https://codepen.io/accudio/pen/JjbNQXw" target="_blank" rel="noopener">See the Pen</a></p></iframe>
<p>As we are going to be using JavaScript to 'enhance' this with the colours of our animation, we're starting with greys that roughly match the tone of our colours.<br>
This helps us in a couple different situations:</p>
<ul>
<li>If the visitor has JS disabled or our JS doesn't load, we can be sure the colour contrast is sufficient and the site accessible.</li>
<li>For performance reasons, we don't want to include our JS blocking the render in the <code>&lt;head&gt;</code>. That means that for a brief period our fallback colours might be displayed before the JS kicks in. By choosing neutral greys it looks more natural than going from one colour to another—like the saturation is turned up from 0.</li>
</ul>
<h2>Colour Config</h2>
<p>So we can access them with JS later, I'm configuring my colours in the JS:</p>
<pre class="language-js"><code class="language-js"><span class="token keyword">const</span> config <span class="token operator">=</span> <span class="token punctuation">{</span><br>	<span class="token literal-property property">states</span><span class="token operator">:</span> <span class="token punctuation">[</span><br>		<span class="token punctuation">{</span><br>			<span class="token literal-property property">at</span><span class="token operator">:</span> <span class="token number">0</span><span class="token punctuation">,</span><br>			<span class="token literal-property property">name</span><span class="token operator">:</span> <span class="token string">'night'</span><span class="token punctuation">,</span><br>			<span class="token literal-property property">colours</span><span class="token operator">:</span> <span class="token punctuation">{</span><br>				<span class="token literal-property property">c0</span><span class="token operator">:</span> <span class="token string">'#7da5d5'</span><span class="token punctuation">,</span><br>				<span class="token literal-property property">c1</span><span class="token operator">:</span> <span class="token string">'#0c4e8f'</span><span class="token punctuation">,</span><br>				<span class="token literal-property property">c2</span><span class="token operator">:</span> <span class="token string">'#00101f'</span><br>			<span class="token punctuation">}</span><br>		<span class="token punctuation">}</span><span class="token punctuation">,</span><br>		<span class="token punctuation">{</span><br>			<span class="token literal-property property">at</span><span class="token operator">:</span> <span class="token number">6</span><span class="token punctuation">,</span><br>			<span class="token literal-property property">name</span><span class="token operator">:</span> <span class="token string">'sunrise'</span><span class="token punctuation">,</span><br>			<span class="token literal-property property">colours</span><span class="token operator">:</span> <span class="token punctuation">{</span><br>				<span class="token literal-property property">c0</span><span class="token operator">:</span> <span class="token string">'#fed4d5'</span><span class="token punctuation">,</span><br>				<span class="token literal-property property">c1</span><span class="token operator">:</span> <span class="token string">'#a496c4'</span><span class="token punctuation">,</span><br>				<span class="token literal-property property">c2</span><span class="token operator">:</span> <span class="token string">'#2e2c3f'</span><br>			<span class="token punctuation">}</span><br>		<span class="token punctuation">}</span><span class="token punctuation">,</span><br>		<span class="token punctuation">{</span><br>			<span class="token literal-property property">at</span><span class="token operator">:</span> <span class="token number">12</span><span class="token punctuation">,</span><br>			<span class="token literal-property property">name</span><span class="token operator">:</span> <span class="token string">'day'</span><span class="token punctuation">,</span><br>			<span class="token literal-property property">colours</span><span class="token operator">:</span> <span class="token punctuation">{</span><br>				<span class="token literal-property property">c0</span><span class="token operator">:</span> <span class="token string">'#ffe2a6'</span><span class="token punctuation">,</span><br>				<span class="token literal-property property">c1</span><span class="token operator">:</span> <span class="token string">'#fc813a'</span><span class="token punctuation">,</span><br>				<span class="token literal-property property">c2</span><span class="token operator">:</span> <span class="token string">'#2f1121'</span><br>			<span class="token punctuation">}</span><br>		<span class="token punctuation">}</span><span class="token punctuation">,</span><br>		<span class="token punctuation">{</span><br>			<span class="token literal-property property">at</span><span class="token operator">:</span> <span class="token number">18</span><span class="token punctuation">,</span><br>			<span class="token literal-property property">name</span><span class="token operator">:</span> <span class="token string">'sunset'</span><span class="token punctuation">,</span><br>			<span class="token literal-property property">colours</span><span class="token operator">:</span> <span class="token punctuation">{</span><br>				<span class="token literal-property property">c0</span><span class="token operator">:</span> <span class="token string">'#ffad39'</span><span class="token punctuation">,</span><br>				<span class="token literal-property property">c1</span><span class="token operator">:</span> <span class="token string">'#e17b17'</span><span class="token punctuation">,</span><br>				<span class="token literal-property property">c2</span><span class="token operator">:</span> <span class="token string">'#1e0000'</span><br>			<span class="token punctuation">}</span><br>		<span class="token punctuation">}</span><br>	<span class="token punctuation">]</span><br><span class="token punctuation">}</span></code></pre>
<p>We'll add to this later, and the <code>at</code> property will become more clear with more code below. We are defining an array of different themes, giving each a name so we can look them up later, and defining our colour palette.</p>
<p>My website has 10 unique colours, I have reduced it to 3 in code snippets for simplicity. If you're interested in all 10 have a look at the CodePens!</p>
<h2>Animating Custom Properties</h2>
<p>In CSS we have the <code>animation</code> and <code>transition</code> properties. These help us animate between two values without needing JS. We should be able to use that to animate our custom properties right? Unfortunately, not right.</p>
<p>As great as custom properties are, at the moment they have limits. One of those limits is in animation or transitions. At the moment custom properties are strings, so the browser transition engine can't know how to <em>interpolate</em> between two values when they change.</p>
<p>This is one of the things that the <a href="https://developer.mozilla.org/en-US/docs/Web/Houdini">Houdini Project</a> is designed to solve, but it is currently Blink-only so that's not well-supported enough for us at the moment. The idea is you specify exactly the type of value a property represents (eg, colour) and the browser can handle interpolating it.</p>
<h2>The Animation</h2>
<p>I found it difficult to tutorial-ise the animation JS so what  I'm going to do is include my commented code. Feel free to go back to the CodePen above and have a dig around yourself, or get in touch if you have any questions!</p>
<pre class="language-js"><code class="language-js"><span class="token comment">// Configuration of colours and animation states</span><br><span class="token keyword">const</span> config <span class="token operator">=</span> <span class="token punctuation">{</span><br>	<span class="token comment">// sets the setInterval interval and the progress function for each animation mode</span><br>	<span class="token literal-property property">anims</span><span class="token operator">:</span> <span class="token punctuation">{</span><br>		<span class="token literal-property property">live</span><span class="token operator">:</span> <span class="token punctuation">{</span><br>			<span class="token comment">// A high interval as live changes very infrequently.</span><br>			<span class="token literal-property property">interval</span><span class="token operator">:</span> <span class="token number">60000</span><span class="token punctuation">,</span><br>			<span class="token function-variable function">getProgress</span><span class="token operator">:</span> <span class="token parameter">now</span> <span class="token operator">=></span> <span class="token punctuation">{</span><br>				<span class="token comment">// Current seconds elapsed this day, divided by number of seconds in the day</span><br>				<span class="token keyword">const</span> time <span class="token operator">=</span> <span class="token punctuation">(</span>now<span class="token punctuation">.</span><span class="token function">getHours</span><span class="token punctuation">(</span><span class="token punctuation">)</span> <span class="token operator">*</span> <span class="token number">3600</span><span class="token punctuation">)</span> <span class="token operator">+</span> <span class="token punctuation">(</span>now<span class="token punctuation">.</span><span class="token function">getMinutes</span><span class="token punctuation">(</span><span class="token punctuation">)</span> <span class="token operator">*</span> <span class="token number">60</span><span class="token punctuation">)</span> <span class="token operator">+</span> now<span class="token punctuation">.</span><span class="token function">getSeconds</span><span class="token punctuation">(</span><span class="token punctuation">)</span><br>				<span class="token keyword">return</span> time <span class="token operator">/</span> <span class="token number">86400</span><br>			<span class="token punctuation">}</span><br>		<span class="token punctuation">}</span><span class="token punctuation">,</span><br>		<span class="token literal-property property">cycle</span><span class="token operator">:</span> <span class="token punctuation">{</span><br>			<span class="token comment">// A low interval as cycle changes in milliseconds.</span><br>			<span class="token literal-property property">interval</span><span class="token operator">:</span> <span class="token number">50</span><span class="token punctuation">,</span><br>			<span class="token function-variable function">getProgress</span><span class="token operator">:</span> <span class="token parameter">now</span> <span class="token operator">=></span> <span class="token punctuation">{</span><br>				<span class="token comment">// Current milliseconss elapsed this minute, divided by number of milliseconds in a minute</span><br>				<span class="token keyword">const</span> time <span class="token operator">=</span> <span class="token punctuation">(</span>now<span class="token punctuation">.</span><span class="token function">getSeconds</span><span class="token punctuation">(</span><span class="token punctuation">)</span> <span class="token operator">*</span> <span class="token number">1000</span><span class="token punctuation">)</span> <span class="token operator">+</span> now<span class="token punctuation">.</span><span class="token function">getMilliseconds</span><span class="token punctuation">(</span><span class="token punctuation">)</span><br>				<span class="token keyword">return</span> time <span class="token operator">/</span> <span class="token number">60000</span><br>			<span class="token punctuation">}</span><br>		<span class="token punctuation">}</span><br>	<span class="token punctuation">}</span><span class="token punctuation">,</span><br>	<span class="token comment">// States with 'at' specifying the time in hours the state should be.</span><br>	<span class="token comment">// 'name' allows referring to it when we add themes later.</span><br>	<span class="token comment">// 'colours' is object with key as custom property name and value as colour.</span><br>	<span class="token literal-property property">states</span><span class="token operator">:</span> <span class="token punctuation">[</span><br>		<span class="token punctuation">{</span><br>			<span class="token literal-property property">at</span><span class="token operator">:</span> <span class="token number">0</span><span class="token punctuation">,</span><br>			<span class="token literal-property property">name</span><span class="token operator">:</span> <span class="token string">'night'</span><span class="token punctuation">,</span><br>			<span class="token literal-property property">colours</span><span class="token operator">:</span> <span class="token punctuation">{</span><br>				<span class="token literal-property property">c0</span><span class="token operator">:</span> <span class="token string">'#7da5d5'</span><span class="token punctuation">,</span><br>				<span class="token literal-property property">c1</span><span class="token operator">:</span> <span class="token string">'#0c4e8f'</span><span class="token punctuation">,</span><br>				<span class="token literal-property property">c2</span><span class="token operator">:</span> <span class="token string">'#00101f'</span><br>			<span class="token punctuation">}</span><br>		<span class="token punctuation">}</span><span class="token punctuation">,</span><br>		<span class="token punctuation">{</span><br>			<span class="token literal-property property">at</span><span class="token operator">:</span> <span class="token number">6</span><span class="token punctuation">,</span><br>			<span class="token literal-property property">name</span><span class="token operator">:</span> <span class="token string">'sunrise'</span><span class="token punctuation">,</span><br>			<span class="token literal-property property">colours</span><span class="token operator">:</span> <span class="token punctuation">{</span><br>				<span class="token literal-property property">c0</span><span class="token operator">:</span> <span class="token string">'#fed4d5'</span><span class="token punctuation">,</span><br>				<span class="token literal-property property">c1</span><span class="token operator">:</span> <span class="token string">'#a496c4'</span><span class="token punctuation">,</span><br>				<span class="token literal-property property">c2</span><span class="token operator">:</span> <span class="token string">'#2e2c3f'</span><br>			<span class="token punctuation">}</span><br>		<span class="token punctuation">}</span><span class="token punctuation">,</span><br>		<span class="token punctuation">{</span><br>			<span class="token literal-property property">at</span><span class="token operator">:</span> <span class="token number">12</span><span class="token punctuation">,</span><br>			<span class="token literal-property property">name</span><span class="token operator">:</span> <span class="token string">'day'</span><span class="token punctuation">,</span><br>			<span class="token literal-property property">colours</span><span class="token operator">:</span> <span class="token punctuation">{</span><br>				<span class="token literal-property property">c0</span><span class="token operator">:</span> <span class="token string">'#ffe2a6'</span><span class="token punctuation">,</span><br>				<span class="token literal-property property">c1</span><span class="token operator">:</span> <span class="token string">'#fc813a'</span><span class="token punctuation">,</span><br>				<span class="token literal-property property">c2</span><span class="token operator">:</span> <span class="token string">'#2f1121'</span><br>			<span class="token punctuation">}</span><br>		<span class="token punctuation">}</span><span class="token punctuation">,</span><br>		<span class="token punctuation">{</span><br>			<span class="token literal-property property">at</span><span class="token operator">:</span> <span class="token number">18</span><span class="token punctuation">,</span><br>			<span class="token literal-property property">name</span><span class="token operator">:</span> <span class="token string">'sunset'</span><span class="token punctuation">,</span><br>			<span class="token literal-property property">colours</span><span class="token operator">:</span> <span class="token punctuation">{</span><br>				<span class="token literal-property property">c0</span><span class="token operator">:</span> <span class="token string">'#ffad39'</span><span class="token punctuation">,</span><br>				<span class="token literal-property property">c1</span><span class="token operator">:</span> <span class="token string">'#e17b17'</span><span class="token punctuation">,</span><br>				<span class="token literal-property property">c2</span><span class="token operator">:</span> <span class="token string">'#1e0000'</span><br>			<span class="token punctuation">}</span><br>		<span class="token punctuation">}</span><br>	<span class="token punctuation">]</span><br><span class="token punctuation">}</span><br><br><span class="token keyword">const</span> root <span class="token operator">=</span> document<span class="token punctuation">.</span>documentElement<br><br><span class="token comment">// This changes the interval and progress calculation between</span><br><span class="token comment">// our dynamic animations 'live' and 'cycle'.</span><br><span class="token keyword">let</span> animMode <span class="token operator">=</span> <span class="token string">'live'</span><br><br><span class="token comment">// Add first element of states to end so we have a seamless loop:</span><br><span class="token comment">// night > sunrise > day > sunset > night</span><br>config<span class="token punctuation">.</span>states<span class="token punctuation">.</span><span class="token function">push</span><span class="token punctuation">(</span><span class="token punctuation">{</span><br>	<span class="token operator">...</span>config<span class="token punctuation">.</span>states<span class="token punctuation">[</span><span class="token number">0</span><span class="token punctuation">]</span><span class="token punctuation">,</span><br>	<span class="token literal-property property">name</span><span class="token operator">:</span> <span class="token string">'end'</span><span class="token punctuation">,</span><br>	<span class="token literal-property property">at</span><span class="token operator">:</span> <span class="token number">24</span><br><span class="token punctuation">}</span><span class="token punctuation">)</span><br><br><span class="token comment">// Declaring our animation loop in a variable allows us to end it when needed.</span><br><span class="token keyword">let</span> animation<br><span class="token keyword">function</span> <span class="token function">startAnim</span><span class="token punctuation">(</span><span class="token punctuation">)</span> <span class="token punctuation">{</span><br>	<span class="token comment">// Run our update loop immediately after starting.</span><br>	<span class="token function">updateAnim</span><span class="token punctuation">(</span><span class="token punctuation">)</span><br><br>	<span class="token comment">// setInterval runs our update loop with a predetermined interval</span><br>	<span class="token comment">// based on the animation mode we are using.</span><br>	animation <span class="token operator">=</span> <span class="token function">setInterval</span><span class="token punctuation">(</span>updateAnim<span class="token punctuation">,</span> config<span class="token punctuation">.</span>anims<span class="token punctuation">[</span>animMode<span class="token punctuation">]</span><span class="token punctuation">.</span>interval<span class="token punctuation">)</span><br><span class="token punctuation">}</span><br><br><span class="token comment">// If we need to end the animation, this function will stop it</span><br><span class="token comment">// running again using clearInterval</span><br><span class="token keyword">function</span> <span class="token function">endAnim</span><span class="token punctuation">(</span><span class="token punctuation">)</span> <span class="token punctuation">{</span><br>	<span class="token function">clearInterval</span><span class="token punctuation">(</span>animation<span class="token punctuation">)</span><br><span class="token punctuation">}</span><br><br><span class="token comment">// This runs every update cycle, getting the progress, calculating</span><br><span class="token comment">// the right colours and applying them to the root element</span><br><span class="token keyword">function</span> <span class="token function">updateAnim</span><span class="token punctuation">(</span><span class="token punctuation">)</span> <span class="token punctuation">{</span><br>	<span class="token comment">// Get the progress through the animation. getProgress returns a number between 0 and 1.</span><br>	<span class="token comment">// To simplify working with time, we multiply this by 24 to get progress through the day.</span><br>	<span class="token keyword">const</span> progress <span class="token operator">=</span> <span class="token function">getProgress</span><span class="token punctuation">(</span><span class="token punctuation">)</span> <span class="token operator">*</span> <span class="token number">24</span><br><br>	<span class="token comment">// Find the next 'state' we are transitioning to based on the 'at' property.</span><br>	<span class="token comment">// The 'at' property sets at what hour that state should be at.</span><br>	<span class="token keyword">const</span> nextIndex <span class="token operator">=</span> config<span class="token punctuation">.</span>states<span class="token punctuation">.</span><span class="token function">findIndex</span><span class="token punctuation">(</span><span class="token parameter">frame</span> <span class="token operator">=></span> <span class="token punctuation">{</span><br>		<span class="token keyword">return</span> frame<span class="token punctuation">.</span>at <span class="token operator">!==</span> <span class="token number">0</span> <span class="token operator">&amp;&amp;</span> progress <span class="token operator">&lt;</span> frame<span class="token punctuation">.</span>at<br>	<span class="token punctuation">}</span><span class="token punctuation">)</span><br>	<span class="token comment">// The previous 'state' is the one before the next one, so we remove 1.</span><br>	<span class="token keyword">const</span> lastIndex <span class="token operator">=</span> nextIndex <span class="token operator">-</span> <span class="token number">1</span><br><br>	<span class="token comment">// Get the onjects for the last and next states</span><br>	<span class="token keyword">const</span> lastState <span class="token operator">=</span> config<span class="token punctuation">.</span>states<span class="token punctuation">[</span>lastIndex<span class="token punctuation">]</span><br>	<span class="token keyword">const</span> nextState <span class="token operator">=</span> config<span class="token punctuation">.</span>states<span class="token punctuation">[</span>nextIndex<span class="token punctuation">]</span><br><br>	<span class="token comment">// Calculate the difference between the 'at' values of the previous and last states,</span><br>	<span class="token comment">// so we can get our progress between them based on the progress we got above.</span><br>	<span class="token keyword">const</span> diff <span class="token operator">=</span> nextState<span class="token punctuation">.</span>at <span class="token operator">-</span> lastState<span class="token punctuation">.</span>at<br>	<span class="token keyword">const</span> progressCurr <span class="token operator">=</span> <span class="token punctuation">(</span>progress <span class="token operator">-</span> lastState<span class="token punctuation">.</span>at<span class="token punctuation">)</span> <span class="token operator">/</span> diff<br><br>	<span class="token comment">// Loop through all the colours. 'key' is the cutsom property name</span><br>	Object<span class="token punctuation">.</span><span class="token function">keys</span><span class="token punctuation">(</span>lastState<span class="token punctuation">.</span>colours<span class="token punctuation">)</span><span class="token punctuation">.</span><span class="token function">forEach</span><span class="token punctuation">(</span><span class="token parameter">key</span> <span class="token operator">=></span> <span class="token punctuation">{</span><br>		<span class="token comment">// We use hex codes for colours for convenience, but it's a lot easier to transition</span><br>		<span class="token comment">// seperate Red, Green, Blue values so we convert them to a [R, G, B] array</span><br>		<span class="token keyword">const</span> lastRGB <span class="token operator">=</span> <span class="token function">hexToRgb</span><span class="token punctuation">(</span>lastState<span class="token punctuation">.</span>colours<span class="token punctuation">[</span>key<span class="token punctuation">]</span><span class="token punctuation">)</span><br>		<span class="token keyword">const</span> nextRGB <span class="token operator">=</span> <span class="token function">hexToRgb</span><span class="token punctuation">(</span>nextState<span class="token punctuation">.</span>colours<span class="token punctuation">[</span>key<span class="token punctuation">]</span><span class="token punctuation">)</span><br><br>		<span class="token comment">// Get the new RGB by using 'lerping' to find the value between the last and next</span><br>		<span class="token comment">// colours based on how far we are through the current animation.</span><br>		<span class="token comment">// The lerp function doesn't necessarily return an int so we round it.</span><br>		<span class="token keyword">const</span> currRGB <span class="token operator">=</span> <span class="token punctuation">[</span><br>			Math<span class="token punctuation">.</span><span class="token function">round</span><span class="token punctuation">(</span><span class="token function">lerp</span><span class="token punctuation">(</span>lastRGB<span class="token punctuation">[</span><span class="token number">0</span><span class="token punctuation">]</span><span class="token punctuation">,</span> nextRGB<span class="token punctuation">[</span><span class="token number">0</span><span class="token punctuation">]</span><span class="token punctuation">,</span> progressCurr<span class="token punctuation">)</span><span class="token punctuation">)</span><span class="token punctuation">,</span><br>			Math<span class="token punctuation">.</span><span class="token function">round</span><span class="token punctuation">(</span><span class="token function">lerp</span><span class="token punctuation">(</span>lastRGB<span class="token punctuation">[</span><span class="token number">1</span><span class="token punctuation">]</span><span class="token punctuation">,</span> nextRGB<span class="token punctuation">[</span><span class="token number">1</span><span class="token punctuation">]</span><span class="token punctuation">,</span> progressCurr<span class="token punctuation">)</span><span class="token punctuation">)</span><span class="token punctuation">,</span><br>			Math<span class="token punctuation">.</span><span class="token function">round</span><span class="token punctuation">(</span><span class="token function">lerp</span><span class="token punctuation">(</span>lastRGB<span class="token punctuation">[</span><span class="token number">2</span><span class="token punctuation">]</span><span class="token punctuation">,</span> nextRGB<span class="token punctuation">[</span><span class="token number">2</span><span class="token punctuation">]</span><span class="token punctuation">,</span> progressCurr<span class="token punctuation">)</span><span class="token punctuation">)</span><br>		<span class="token punctuation">]</span><br><br>		<span class="token comment">// Apply the custom property to root using the name and our new RGB value.</span><br>		<span class="token function">applyColour</span><span class="token punctuation">(</span>key<span class="token punctuation">,</span> currRGB<span class="token punctuation">)</span><br>	<span class="token punctuation">}</span><span class="token punctuation">)</span><br><span class="token punctuation">}</span><br><br><span class="token comment">// As we have two different animation 'modes', we change the function used to work</span><br><span class="token comment">// out the progress depending on that mode. See the config above for how they work.</span><br><span class="token keyword">function</span> <span class="token function">getProgress</span><span class="token punctuation">(</span><span class="token punctuation">)</span> <span class="token punctuation">{</span><br>	<span class="token keyword">const</span> d <span class="token operator">=</span> <span class="token keyword">new</span> <span class="token class-name">Date</span><span class="token punctuation">(</span><span class="token punctuation">)</span><br>	<span class="token keyword">const</span> progress <span class="token operator">=</span> config<span class="token punctuation">.</span>anims<span class="token punctuation">[</span>animMode<span class="token punctuation">]</span><span class="token punctuation">.</span><span class="token function">getProgress</span><span class="token punctuation">(</span>d<span class="token punctuation">)</span><br><br>	<span class="token keyword">return</span> progress<br><span class="token punctuation">}</span><br><br><span class="token comment">// A slightly bewildering regular expression that turns a hex code into [R, G. B] array.</span><br><span class="token comment">// Well-tested though so I don't need to touch it!</span><br><span class="token keyword">function</span> <span class="token function">hexToRgb</span><span class="token punctuation">(</span><span class="token parameter">hex</span><span class="token punctuation">)</span> <span class="token punctuation">{</span><br>	<span class="token keyword">var</span> result <span class="token operator">=</span> <span class="token regex"><span class="token regex-delimiter">/</span><span class="token regex-source language-regex">^#?([a-f\d]{2})([a-f\d]{2})([a-f\d]{2})$</span><span class="token regex-delimiter">/</span><span class="token regex-flags">i</span></span><span class="token punctuation">.</span><span class="token function">exec</span><span class="token punctuation">(</span>hex<span class="token punctuation">)</span><br>	<span class="token keyword">return</span> result <span class="token operator">?</span> <span class="token punctuation">[</span><br>		<span class="token function">parseInt</span><span class="token punctuation">(</span>result<span class="token punctuation">[</span><span class="token number">1</span><span class="token punctuation">]</span><span class="token punctuation">,</span> <span class="token number">16</span><span class="token punctuation">)</span><span class="token punctuation">,</span><br>		<span class="token function">parseInt</span><span class="token punctuation">(</span>result<span class="token punctuation">[</span><span class="token number">2</span><span class="token punctuation">]</span><span class="token punctuation">,</span> <span class="token number">16</span><span class="token punctuation">)</span><span class="token punctuation">,</span><br>		<span class="token function">parseInt</span><span class="token punctuation">(</span>result<span class="token punctuation">[</span><span class="token number">3</span><span class="token punctuation">]</span><span class="token punctuation">,</span> <span class="token number">16</span><span class="token punctuation">)</span><br>	<span class="token punctuation">]</span> <span class="token operator">:</span> <span class="token keyword">null</span><br><span class="token punctuation">}</span><br><br><span class="token comment">// Using 'linear interpolation' gets the value between the start and end values based on progress</span><br><span class="token keyword">function</span> <span class="token function">lerp</span><span class="token punctuation">(</span><span class="token parameter">start<span class="token punctuation">,</span> end<span class="token punctuation">,</span> progress</span><span class="token punctuation">)</span> <span class="token punctuation">{</span><br>	<span class="token keyword">return</span> <span class="token punctuation">(</span><span class="token number">1</span> <span class="token operator">-</span> progress<span class="token punctuation">)</span> <span class="token operator">*</span> start <span class="token operator">+</span> progress <span class="token operator">*</span> end<br><span class="token punctuation">}</span><br><br><span class="token comment">// Uses name of custom property 'key' and [R, G, B] array and applies to root element</span><br><span class="token keyword">function</span> <span class="token function">applyColour</span><span class="token punctuation">(</span><span class="token parameter">key<span class="token punctuation">,</span> colour</span><span class="token punctuation">)</span> <span class="token punctuation">{</span><br>	<span class="token keyword">const</span> colourString <span class="token operator">=</span> <span class="token string">'rgb('</span> <span class="token operator">+</span> colour<span class="token punctuation">.</span><span class="token function">join</span><span class="token punctuation">(</span><span class="token string">','</span><span class="token punctuation">)</span> <span class="token operator">+</span> <span class="token string">')'</span><br>	root<span class="token punctuation">.</span>style<span class="token punctuation">.</span><span class="token function">setProperty</span><span class="token punctuation">(</span><span class="token string">'--'</span> <span class="token operator">+</span> key<span class="token punctuation">,</span> colourString<span class="token punctuation">)</span><br><span class="token punctuation">}</span><br><br><span class="token comment">// Round number to 'places' number of figures after decimal.</span><br><span class="token keyword">function</span> <span class="token function">round</span><span class="token punctuation">(</span><span class="token parameter">num<span class="token punctuation">,</span> places</span><span class="token punctuation">)</span> <span class="token punctuation">{</span><br>	<span class="token keyword">const</span> power <span class="token operator">=</span> Math<span class="token punctuation">.</span><span class="token function">pow</span><span class="token punctuation">(</span><span class="token number">10</span><span class="token punctuation">,</span> places<span class="token punctuation">)</span><br>	<span class="token keyword">return</span> Math<span class="token punctuation">.</span><span class="token function">round</span><span class="token punctuation">(</span>num <span class="token operator">*</span> power<span class="token punctuation">)</span> <span class="token operator">/</span> power<br><span class="token punctuation">}</span><br><br><span class="token comment">// Initialise and start animation.</span><br><span class="token keyword">function</span> <span class="token function">init</span><span class="token punctuation">(</span><span class="token punctuation">)</span> <span class="token punctuation">{</span><br>	<span class="token function">startAnim</span><span class="token punctuation">(</span><span class="token punctuation">)</span><br><span class="token punctuation">}</span><br><span class="token function">init</span><span class="token punctuation">(</span><span class="token punctuation">)</span></code></pre>
<h2>Theme Picker</h2>
<p>With the above code, we have an animated live colour scheme and the flexibility to extend it further. Let's do just that by creating methods to switch between 'dynamic' schemes and our named states.</p>
<p>We'll go through the basic code to change, and then a basic 'theme picker'.</p>
<h3>Switching between dynamic states</h3>
<p>In our configuration, we have set the progress function and interval for each dynamic theme. When we start the animation and when our <code>updateAnim()</code> function run, they use the value of <code>animMode</code> to choose the correct interval and progress function for the current mode.</p>
<p>This means all we need to do is stop the animation, change <code>animMode</code>, and start it again. For example to change to 'cycle':</p>
<pre class="language-js"><code class="language-js"><span class="token function">endAnim</span><span class="token punctuation">(</span><span class="token punctuation">)</span><br>animMode <span class="token operator">=</span> <span class="token string">'cycle'</span><br><span class="token function">startAnim</span><span class="token punctuation">(</span><span class="token punctuation">)</span></code></pre>
<p>And likewise, to switch to 'live', we would do the same process but instead set <code>animMode</code> to 'live'.</p>
<h3>Switching to a static 'named' state</h3>
<p>We included the name property within our state so that we can refer to it when setting the theme. First we need to stop the animation, so that the dynamic state doesn't replace our changes when it next runs. Then, we need to find the colours for the state we would like to apply and apply them. We can do that with this short piece of code.</p>
<pre class="language-js"><code class="language-js"><span class="token keyword">const</span> theme <span class="token operator">=</span> <span class="token string">'sunset'</span><br><span class="token function">endAnim</span><span class="token punctuation">(</span><span class="token punctuation">)</span><br><span class="token keyword">const</span> state <span class="token operator">=</span> config<span class="token punctuation">.</span>states<span class="token punctuation">.</span><span class="token function">find</span><span class="token punctuation">(</span><span class="token parameter">item</span> <span class="token operator">=></span> item<span class="token punctuation">.</span>name <span class="token operator">===</span> theme<span class="token punctuation">)</span><br>Object<span class="token punctuation">.</span><span class="token function">keys</span><span class="token punctuation">(</span>state<span class="token punctuation">.</span>colours<span class="token punctuation">)</span><span class="token punctuation">.</span><span class="token function">forEach</span><span class="token punctuation">(</span><span class="token parameter">key</span> <span class="token operator">=></span> <span class="token punctuation">{</span><br>	<span class="token function">applyColour</span><span class="token punctuation">(</span>key<span class="token punctuation">,</span> <span class="token function">hexToRgb</span><span class="token punctuation">(</span>state<span class="token punctuation">.</span>colours<span class="token punctuation">[</span>key<span class="token punctuation">]</span><span class="token punctuation">)</span><span class="token punctuation">)</span><br><span class="token punctuation">}</span><span class="token punctuation">)</span></code></pre>
<p>Line 3 uses the handy Array method 'find' which will return the item that matches our condition: where <code>item.name</code> equals our theme name.<br>
We then loop through all the colours of that state and apply them as we did for our dynamic 'themes'.</p>
<h3>Theme Picker</h3>
<p>It's worth building out a theme picker for yourself, but here's a simple implementation to get us started:</p>
<pre class="language-html"><code class="language-html"><span class="token tag"><span class="token tag"><span class="token punctuation">&lt;</span>button</span> <span class="token attr-name">data-active</span> <span class="token attr-name">aria-pressed</span> <span class="token attr-name">data-theme</span><span class="token attr-value"><span class="token punctuation attr-equals">=</span><span class="token punctuation">"</span>live<span class="token punctuation">"</span></span><span class="token punctuation">></span></span>Live<span class="token tag"><span class="token tag"><span class="token punctuation">&lt;/</span>button</span><span class="token punctuation">></span></span><br><span class="token tag"><span class="token tag"><span class="token punctuation">&lt;</span>button</span> <span class="token attr-name">data-theme</span><span class="token attr-value"><span class="token punctuation attr-equals">=</span><span class="token punctuation">"</span>cycle<span class="token punctuation">"</span></span><span class="token punctuation">></span></span>Cycle<span class="token tag"><span class="token tag"><span class="token punctuation">&lt;/</span>button</span><span class="token punctuation">></span></span><br><span class="token tag"><span class="token tag"><span class="token punctuation">&lt;</span>button</span> <span class="token attr-name">data-theme</span><span class="token attr-value"><span class="token punctuation attr-equals">=</span><span class="token punctuation">"</span>sunrise<span class="token punctuation">"</span></span><span class="token punctuation">></span></span>Sunrise<span class="token tag"><span class="token tag"><span class="token punctuation">&lt;/</span>button</span><span class="token punctuation">></span></span><br><span class="token tag"><span class="token tag"><span class="token punctuation">&lt;</span>button</span> <span class="token attr-name">data-theme</span><span class="token attr-value"><span class="token punctuation attr-equals">=</span><span class="token punctuation">"</span>day<span class="token punctuation">"</span></span><span class="token punctuation">></span></span>Day<span class="token tag"><span class="token tag"><span class="token punctuation">&lt;/</span>button</span><span class="token punctuation">></span></span><br><span class="token tag"><span class="token tag"><span class="token punctuation">&lt;</span>button</span> <span class="token attr-name">data-theme</span><span class="token attr-value"><span class="token punctuation attr-equals">=</span><span class="token punctuation">"</span>sunset<span class="token punctuation">"</span></span><span class="token punctuation">></span></span>Sunset<span class="token tag"><span class="token tag"><span class="token punctuation">&lt;/</span>button</span><span class="token punctuation">></span></span><br><span class="token tag"><span class="token tag"><span class="token punctuation">&lt;</span>button</span> <span class="token attr-name">data-theme</span><span class="token attr-value"><span class="token punctuation attr-equals">=</span><span class="token punctuation">"</span>night<span class="token punctuation">"</span></span><span class="token punctuation">></span></span>Night<span class="token tag"><span class="token tag"><span class="token punctuation">&lt;/</span>button</span><span class="token punctuation">></span></span></code></pre>
<pre class="language-js"><code class="language-js"><span class="token keyword">const</span> themes <span class="token operator">=</span> document<span class="token punctuation">.</span><span class="token function">querySelectorAll</span><span class="token punctuation">(</span><span class="token string">'[data-theme]'</span><span class="token punctuation">)</span><br><span class="token keyword">if</span> <span class="token punctuation">(</span>themes<span class="token punctuation">)</span> <span class="token punctuation">{</span><br>	themes<span class="token punctuation">.</span><span class="token function">forEach</span><span class="token punctuation">(</span><span class="token keyword">function</span><span class="token punctuation">(</span><span class="token parameter">theme</span><span class="token punctuation">)</span> <span class="token punctuation">{</span><br>		theme<span class="token punctuation">.</span><span class="token function">addEventListener</span><span class="token punctuation">(</span><span class="token string">'click'</span><span class="token punctuation">,</span> <span class="token keyword">function</span><span class="token punctuation">(</span><span class="token parameter">e</span><span class="token punctuation">)</span> <span class="token punctuation">{</span><br>			<span class="token comment">// remove active state from old theme buttons</span><br>			themes<span class="token punctuation">.</span><span class="token function">forEach</span><span class="token punctuation">(</span><span class="token parameter">theme</span> <span class="token operator">=></span> <span class="token punctuation">{</span><br>				theme<span class="token punctuation">.</span><span class="token function">removeAttribute</span><span class="token punctuation">(</span><span class="token string">'data-active'</span><span class="token punctuation">)</span><br>				theme<span class="token punctuation">.</span><span class="token function">removeAttribute</span><span class="token punctuation">(</span><span class="token string">'aria-pressed'</span><span class="token punctuation">)</span><br>			<span class="token punctuation">}</span><span class="token punctuation">)</span><br><br>			<span class="token comment">// add active state to clicked button</span><br>			<span class="token keyword">this</span><span class="token punctuation">.</span><span class="token function">setAttribute</span><span class="token punctuation">(</span><span class="token string">'data-active'</span><span class="token punctuation">,</span> <span class="token string">''</span><span class="token punctuation">)</span><br>			<span class="token keyword">this</span><span class="token punctuation">.</span><span class="token function">setAttribute</span><span class="token punctuation">(</span><span class="token string">'aria-pressed'</span><span class="token punctuation">,</span> <span class="token string">''</span><span class="token punctuation">)</span><br><br>			<span class="token comment">// get slug for current theme</span><br>			<span class="token keyword">const</span> themeSlug <span class="token operator">=</span> <span class="token keyword">this</span><span class="token punctuation">.</span><span class="token function">getAttribute</span><span class="token punctuation">(</span><span class="token string">'data-theme'</span><span class="token punctuation">)</span><br><br>			<span class="token comment">// end animation</span><br>			<span class="token function">endAnim</span><span class="token punctuation">(</span><span class="token punctuation">)</span><br><br>			<span class="token comment">// if dynamic theme, set animMode, start animation and return</span><br>			<span class="token keyword">if</span> <span class="token punctuation">(</span>themeSlug <span class="token operator">===</span> <span class="token string">'live'</span> <span class="token operator">||</span> themeSlug <span class="token operator">===</span> <span class="token string">'cycle'</span><span class="token punctuation">)</span> <span class="token punctuation">{</span><br>				animMode <span class="token operator">=</span> themeSlug<br>				<span class="token function">startAnim</span><span class="token punctuation">(</span><span class="token punctuation">)</span><br>				<span class="token keyword">return</span><br>			<span class="token punctuation">}</span><br><br>			<span class="token comment">// find theme state and apply the colours</span><br>			<span class="token keyword">const</span> state <span class="token operator">=</span> config<span class="token punctuation">.</span>states<span class="token punctuation">.</span><span class="token function">find</span><span class="token punctuation">(</span><span class="token parameter">item</span> <span class="token operator">=></span> item<span class="token punctuation">.</span>name <span class="token operator">===</span> themeSlug<span class="token punctuation">)</span><br>			Object<span class="token punctuation">.</span><span class="token function">keys</span><span class="token punctuation">(</span>state<span class="token punctuation">.</span>colours<span class="token punctuation">)</span><span class="token punctuation">.</span><span class="token function">forEach</span><span class="token punctuation">(</span><span class="token parameter">key</span> <span class="token operator">=></span> <span class="token punctuation">{</span><br>				<span class="token function">applyColour</span><span class="token punctuation">(</span>key<span class="token punctuation">,</span> <span class="token function">hexToRgb</span><span class="token punctuation">(</span>state<span class="token punctuation">.</span>colours<span class="token punctuation">[</span>key<span class="token punctuation">]</span><span class="token punctuation">)</span><span class="token punctuation">)</span><br>			<span class="token punctuation">}</span><span class="token punctuation">)</span><br>		<span class="token punctuation">}</span><span class="token punctuation">)</span><br>	<span class="token punctuation">}</span><span class="token punctuation">)</span><br><span class="token punctuation">}</span></code></pre>
<h2>Sun Animation</h2>
<p>The final piece to our landscape is a moving sun. You would have thought it would be easy to implement, but it turned out to be more tricky than I first thought.</p>
<p>Lets go over our requirements:</p>
<ol>
<li>A small circle that is on the far left at 6, top at 12, right at 18, and bottom at 24.</li>
<li>Respects screen width so it works for wide and narrow screens.</li>
<li>Respects screen height so it is positioned in the right place compared to the landscape.</li>
<li>Follows an ellipse based on the width and height</li>
</ol>
<p>Due to all these reasons, my first thought of using animations becomes hard to implement. Respecting width, height and following an ellipse though sounds like a tricky challenge.</p>
<p>The solution ends up using our favourite feature the Custom Property, and exploiting the relationship between ellipses and the Sin function.</p>
<p>We can continue to keep our JavaScript minimal and respect the screen size by using transforms and elements the size of the screen. To our .landscape from the previous post:</p>
<pre class="language-html"><code class="language-html"><span class="token tag"><span class="token tag"><span class="token punctuation">&lt;</span>div</span> <span class="token attr-name">class</span><span class="token attr-value"><span class="token punctuation attr-equals">=</span><span class="token punctuation">"</span>landscape__sunWrap<span class="token punctuation">"</span></span><span class="token punctuation">></span></span><br>	<span class="token tag"><span class="token tag"><span class="token punctuation">&lt;</span>div</span> <span class="token attr-name">class</span><span class="token attr-value"><span class="token punctuation attr-equals">=</span><span class="token punctuation">"</span>landscape__sun<span class="token punctuation">"</span></span><span class="token punctuation">></span></span><span class="token tag"><span class="token tag"><span class="token punctuation">&lt;/</span>div</span><span class="token punctuation">></span></span><br><span class="token tag"><span class="token tag"><span class="token punctuation">&lt;/</span>div</span><span class="token punctuation">></span></span></code></pre>
<pre class="language-scss"><code class="language-scss"><span class="token property"><span class="token variable">$sun-size</span></span><span class="token punctuation">:</span> <span class="token function">min</span><span class="token punctuation">(</span>4rem<span class="token punctuation">,</span> 10vw<span class="token punctuation">)</span><span class="token punctuation">;</span><br><span class="token property"><span class="token variable">$sun-movement-v</span></span><span class="token punctuation">:</span> 30%<span class="token punctuation">;</span><br><span class="token property"><span class="token variable">$sun-movement-h</span></span><span class="token punctuation">:</span> 40%<span class="token punctuation">;</span><br><br><span class="token selector">.landscape </span><span class="token punctuation">{</span><br>	<span class="token selector"><span class="token parent important">&amp;</span>__sunWrap </span><span class="token punctuation">{</span><br>		<span class="token property"><span class="token variable">$distance</span></span><span class="token punctuation">:</span> 10<span class="token punctuation">;</span><br><br>		<span class="token property">bottom</span><span class="token punctuation">:</span> 10%<span class="token punctuation">;</span><br>		<span class="token property">height</span><span class="token punctuation">:</span> 75%<span class="token punctuation">;</span><br>		<span class="token property">left</span><span class="token punctuation">:</span> 0<span class="token punctuation">;</span><br>		<span class="token property">position</span><span class="token punctuation">:</span> absolute<span class="token punctuation">;</span><br>		<span class="token property">transform</span><span class="token punctuation">:</span> <span class="token function">translateY</span><span class="token punctuation">(</span><span class="token function">var</span><span class="token punctuation">(</span>--scrollPos<span class="token punctuation">,</span> 0<span class="token punctuation">)</span><span class="token punctuation">)</span><span class="token punctuation">;</span><br>		<span class="token property">width</span><span class="token punctuation">:</span> 100%<span class="token punctuation">;</span><br><br>		<span class="token atrule"><span class="token rule">@media</span> <span class="token punctuation">(</span><span class="token property">prefers-reduced-motion</span><span class="token punctuation">:</span> reduce<span class="token punctuation">)</span></span> <span class="token punctuation">{</span><br>			<span class="token property">display</span><span class="token punctuation">:</span> none<span class="token punctuation">;</span><br>		<span class="token punctuation">}</span><br>	<span class="token punctuation">}</span><br><br>	<span class="token selector"><span class="token parent important">&amp;</span>__sun </span><span class="token punctuation">{</span><br>		<span class="token property">height</span><span class="token punctuation">:</span> 100%<span class="token punctuation">;</span><br>		<span class="token property">left</span><span class="token punctuation">:</span> 0<span class="token punctuation">;</span><br>		<span class="token property">position</span><span class="token punctuation">:</span> absolute<span class="token punctuation">;</span><br>		<span class="token property">top</span><span class="token punctuation">:</span> 0<span class="token punctuation">;</span><br>		<span class="token property">transform</span><span class="token punctuation">:</span><br>			<span class="token function">translateX</span><span class="token punctuation">(</span><span class="token function">calc</span><span class="token punctuation">(</span><span class="token variable">#{$sun-movement-h}</span> <span class="token operator">*</span> <span class="token function">var</span><span class="token punctuation">(</span>--sun-h<span class="token punctuation">)</span><span class="token punctuation">)</span><span class="token punctuation">)</span><br>			<span class="token function">translateY</span><span class="token punctuation">(</span><span class="token function">calc</span><span class="token punctuation">(</span><span class="token variable">#{$sun-movement-v}</span> <span class="token operator">*</span> <span class="token function">var</span><span class="token punctuation">(</span>--sun-v<span class="token punctuation">)</span><span class="token punctuation">)</span><span class="token punctuation">)</span><span class="token punctuation">;</span><br>		<span class="token property">width</span><span class="token punctuation">:</span> 100%<span class="token punctuation">;</span><br><br>		<span class="token comment">// the actual sun element</span><br>		<span class="token selector"><span class="token parent important">&amp;</span>::before </span><span class="token punctuation">{</span><br>			<span class="token property">background</span><span class="token punctuation">:</span> #fff<span class="token punctuation">;</span><br>			<span class="token property">border-radius</span><span class="token punctuation">:</span> 50%<span class="token punctuation">;</span><br>			<span class="token property">content</span><span class="token punctuation">:</span> <span class="token string">''</span><span class="token punctuation">;</span><br>			<span class="token property">height</span><span class="token punctuation">:</span> <span class="token variable">$sun-size</span><span class="token punctuation">;</span><br>			<span class="token property">left</span><span class="token punctuation">:</span> 50%<span class="token punctuation">;</span><br>			<span class="token property">position</span><span class="token punctuation">:</span> absolute<span class="token punctuation">;</span><br>			<span class="token property">top</span><span class="token punctuation">:</span> 50%<span class="token punctuation">;</span><br>			<span class="token property">transform</span><span class="token punctuation">:</span> <span class="token function">translate</span><span class="token punctuation">(</span>-50%<span class="token punctuation">,</span> -50%<span class="token punctuation">)</span><span class="token punctuation">;</span><br>			<span class="token property">width</span><span class="token punctuation">:</span> <span class="token variable">$sun-size</span><span class="token punctuation">;</span><br>		<span class="token punctuation">}</span><br>	<span class="token punctuation">}</span><br><span class="token punctuation">}</span></code></pre>
<p>Using this code the positioning of our sun is based on rails, constrained by the size of our landscape. <code>--sun-h</code> and <code>--sun-v</code> are numbers between -1 and 1 which are used in the <code>calc</code> within our <code>transform</code> property to set how far up/down and left/right the sun is.</p>
<p>The advantage of using an element filling our landscape means that as the element is narrower, the less the sun moves horizontally. This leaves us with minimal JS:</p>
<pre class="language-js"><code class="language-js"><span class="token keyword">function</span> <span class="token function">sunPos</span><span class="token punctuation">(</span><span class="token parameter">progress</span><span class="token punctuation">)</span> <span class="token punctuation">{</span><br>	<span class="token keyword">const</span> sunWrap <span class="token operator">=</span> document<span class="token punctuation">.</span><span class="token function">querySelector</span><span class="token punctuation">(</span><span class="token string">'.landscape__sunWrap'</span><span class="token punctuation">)</span><br>	<span class="token keyword">if</span> <span class="token punctuation">(</span>sunWrap<span class="token punctuation">)</span> <span class="token punctuation">{</span><br>		<span class="token keyword">const</span> sunH <span class="token operator">=</span> <span class="token operator">-</span>Math<span class="token punctuation">.</span><span class="token function">sin</span><span class="token punctuation">(</span><span class="token number">2</span> <span class="token operator">*</span> Math<span class="token punctuation">.</span><span class="token constant">PI</span> <span class="token operator">*</span> progress <span class="token operator">/</span> <span class="token number">24</span><span class="token punctuation">)</span><br>		<span class="token keyword">const</span> sunV <span class="token operator">=</span> <span class="token operator">-</span>Math<span class="token punctuation">.</span><span class="token function">sin</span><span class="token punctuation">(</span><span class="token number">2</span> <span class="token operator">*</span> Math<span class="token punctuation">.</span><span class="token constant">PI</span> <span class="token operator">*</span> <span class="token punctuation">(</span>progress <span class="token operator">-</span> <span class="token number">6</span><span class="token punctuation">)</span> <span class="token operator">/</span> <span class="token number">24</span><span class="token punctuation">)</span><br>		sunWrap<span class="token punctuation">.</span>style<span class="token punctuation">.</span><span class="token function">setProperty</span><span class="token punctuation">(</span><span class="token string">'--sun-h'</span><span class="token punctuation">,</span> <span class="token function">round</span><span class="token punctuation">(</span>sunH<span class="token punctuation">,</span> <span class="token number">3</span><span class="token punctuation">)</span><span class="token punctuation">)</span><br>		sunWrap<span class="token punctuation">.</span>style<span class="token punctuation">.</span><span class="token function">setProperty</span><span class="token punctuation">(</span><span class="token string">'--sun-v'</span><span class="token punctuation">,</span> <span class="token function">round</span><span class="token punctuation">(</span>sunV<span class="token punctuation">,</span> <span class="token number">3</span><span class="token punctuation">)</span><span class="token punctuation">)</span><br>	<span class="token punctuation">}</span><br><span class="token punctuation">}</span></code></pre>
<p>This involves maths that I'm pretty sure I was taught in High School and University, but I am certain I have almost entirely forgotten! For a square element, this would create a circular movement but by splitting it up into separate components we have our ellipse.</p>
<p>We then run <code>sunPos</code> with our progress in our <code>updateAnim()</code> function and using the <code>state.at</code> property after setting a static theme.</p>
<h2>Conclusion</h2>
<p>If you've gotten this far, congratulations and thank you for sticking with me! Here's our final landscape, as above:</p>
<iframe class="codepen" height="380" style="width:100%;" scrolling="no" title="CodePen Embed" src="https://codepen.io/anon/embed/GRNmbjJ?height=380&theme-id=dark&default-tab=result" frameborder="0" loading="lazy" allowtransparency="true" allowfullscreen="true"><p><a href="https://codepen.io/accudio/pen/GRNmbjJ" target="_blank" rel="noopener">See the Pen</a></p></iframe>
<p>This is not the easiest post to read by any stretch of the imagination, but I wanted to get down a lot of info and I struggled to in a way that felt natural. Initial drafts were tutorial-like before I realised I was writing a 10,000 word tutorial!</p>
<p>I am planning to write more, but will be making them shorter and simpler than this one. Keep an eye out for future posts about:</p>
<ul>
<li>The 'hilly' dividers</li>
<li>Blog setup with 11ty, esbuild and gulp</li>
<li>Implementing mouse-based painting with canvas and JS<br>
and a few more I have planned!</li>
</ul>

      <hr>
      <p>If you have any comments or feedback on this article, let me know! I'd love to hear your thoughts, go ahead and <a href="mailto:alistair@accudio.com">send me an email</a> at alistair@accudio.com or <a href="https://front-end.social/@accudio" rel="noopener noreferrer">contact me on Mastodon</a>.</p>
      <p>The post <a href="https://www.alistairshepherd.uk/writing/parallax-svg-landscape-2/">SVG Landscape with live colour theming - new site part 2</a> appeared first on <a href="https://alistairshepherd.uk">alistairshepherd.uk</a>.</p>
    ]]>
      </content>
    </entry>
  
    
    <entry>
      <title>Conditionally loading a polyfill for :focus-visible</title>
      <link href="https://www.alistairshepherd.uk/writing/focus-visible-conditional-polyfill/"/>
      <updated>2021-05-21T00:00:00Z</updated>
      <id>https://www.alistairshepherd.uk/writing/focus-visible-conditional-polyfill/</id>
      <content type="html">
        <![CDATA[
      <p>The future of focus is <code>:focus-visible</code>! Although <a href="https://caniuse.com/?search=focus-visible">browser support</a> is decent, <a href="https://blogs.igalia.com/mrego/2021/01/28/focus-visible-in-webkit-january-2021">Safari is still working on</a> this important accessibility feature.</p>
<p>In the meantime, we can load the <a href="https://github.com/WICG/focus-visible">WICG focus-visible polyfill</a> to offer improved focus styles in browsers that yet don't support it. Ideally we don't force browsers that support focus-visible to download a polyfill when it's unnecessary - and in future when all modern browsers support the feature, we don't want to ship that redundant code.</p>
<p>Here is a snippet we can use to only load the focus-visible polyfill if it isn't supported! Insert this before the closing <code>&lt;/body&gt;</code> and change the <code>script.src</code> to point to your local copy of the polyfill (or use an asset CDN like <a href="https://www.jsdelivr.com/package/npm/focus-visible">jsdelivr</a>).</p>
<pre class="language-html"><code class="language-html"><span class="token tag"><span class="token tag"><span class="token punctuation">&lt;</span>script</span><span class="token punctuation">></span></span><span class="token script"><span class="token language-javascript"><br>  <span class="token keyword">try</span> <span class="token punctuation">{</span><br>    document<span class="token punctuation">.</span>body<span class="token punctuation">.</span><span class="token function">querySelector</span><span class="token punctuation">(</span><span class="token string">':focus-visible'</span><span class="token punctuation">)</span><span class="token punctuation">;</span><br>  <span class="token punctuation">}</span> <span class="token keyword">catch</span> <span class="token punctuation">(</span>error<span class="token punctuation">)</span> <span class="token punctuation">{</span><br>    <span class="token keyword">var</span> script <span class="token operator">=</span> document<span class="token punctuation">.</span><span class="token function">createElement</span><span class="token punctuation">(</span><span class="token string">'script'</span><span class="token punctuation">)</span><span class="token punctuation">;</span><br>    script<span class="token punctuation">.</span>src <span class="token operator">=</span> <span class="token string">"/js/focus-visible.js"</span><span class="token punctuation">;</span><br>    document<span class="token punctuation">.</span>body<span class="token punctuation">.</span><span class="token function">appendChild</span><span class="token punctuation">(</span>script<span class="token punctuation">)</span><span class="token punctuation">;</span><br>  <span class="token punctuation">}</span><br></span></span><span class="token tag"><span class="token tag"><span class="token punctuation">&lt;/</span>script</span><span class="token punctuation">></span></span></code></pre>
<h2>CSS</h2>
<p>You'll also need to write CSS to handle focus indicators in three circumstances:</p>
<ol>
<li>focus-visible is supported;</li>
<li>focus-visible not supported, but polyfill has been loaded;</li>
<li>focus-visible not supported, polyfill not loaded.</li>
</ol>
<p>This is my setup for these cases:</p>
<pre class="language-css"><code class="language-css"><span class="token comment">/**<br> * My focus styles<br> */</span><br><span class="token selector">:focus</span> <span class="token punctuation">{</span><br>  <span class="token property">outline</span><span class="token punctuation">:</span> 2px dashed currentColor<span class="token punctuation">;</span><br>  <span class="token property">outline-offset</span><span class="token punctuation">:</span> .25rem<span class="token punctuation">;</span><br><span class="token punctuation">}</span><br><br><span class="token comment">/**<br> * When focus-visible is supported:<br> * remove outline when :focus but not :focus-visible<br> */</span><br><span class="token selector">:focus:not(:focus-visible)</span> <span class="token punctuation">{</span><br>  <span class="token property">outline</span><span class="token punctuation">:</span> none<span class="token punctuation">;</span><br><span class="token punctuation">}</span><br><br><span class="token comment">/**<br> * when polyfill loaded:<br> * remove outline when :focus but not .focus-visible<br> */</span><br><span class="token selector">.js-focus-visible :focus:not(.focus-visible)</span> <span class="token punctuation">{</span><br>  <span class="token property">outline</span><span class="token punctuation">:</span> none<span class="token punctuation">;</span><br></code></pre>
<h2>Notes</h2>
<ul>
<li>This does add an additional script to load for browsers without support, but by adding the script as above loads it asyncronously. The browser will prioritise other resources, and the worst case is it isn't loaded by the first interaction and a fallback <code>:focus</code> indicator is shown.</li>
<li>If you have a build step that minifies/optimises your CSS, you may find that the two <code>outline: none</code> declarations are combined into a single rule. Due to how the browser ignores any rules with selectors it doesn't understand, this won't work. You may need to disable optimisation on this step, or in my case I changed one of the <code>outline: none</code> declarations to <code>outline: 0</code>. This CSS works the same, but means they won't be combined into a single rule by most minifiers.</li>
</ul>
<h2>Further reading</h2>
<ul>
<li><a href="https://github.com/WICG/focus-visible">WICG focus-visible polyfill</a></li>
<li><a href="https://matthiasott.com/notes/focus-visible-is-here">Matthias Ott - :focus-visible is here</a></li>
<li><a href="https://developer.mozilla.org/en-US/docs/Web/CSS/:focus-visible">MDN - :focus-visible</a></li>
<li><a href="https://css-tricks.com/almanac/selectors/f/focus-visible/">CSS Tricks Almanac - :focus-visible</a></li>
<li><a href="https://caniuse.com/?search=focus-visible">CanIUse - :focus-visible browser support</a></li>
<li><a href="https://blogs.igalia.com/mrego/2021/01/28/focus-visible-in-webkit-january-2021/">Igalia - :focus-visible in WebKit - January 2021</a></li>
</ul>

      <hr>
      <p>If you have any comments or feedback on this article, let me know! I'd love to hear your thoughts, go ahead and <a href="mailto:alistair@accudio.com">send me an email</a> at alistair@accudio.com or <a href="https://front-end.social/@accudio" rel="noopener noreferrer">contact me on Mastodon</a>.</p>
      <p>The post <a href="https://www.alistairshepherd.uk/writing/focus-visible-conditional-polyfill/">Conditionally loading a polyfill for :focus-visible</a> appeared first on <a href="https://alistairshepherd.uk">alistairshepherd.uk</a>.</p>
    ]]>
      </content>
    </entry>
  
    
    <entry>
      <title>Conditionally loading a native image lazyload polyfill/library</title>
      <link href="https://www.alistairshepherd.uk/writing/image-lazyload-conditional-polyfill/"/>
      <updated>2021-06-11T00:00:00Z</updated>
      <id>https://www.alistairshepherd.uk/writing/image-lazyload-conditional-polyfill/</id>
      <content type="html">
        <![CDATA[
      <p>My <a href="/writing/focus-visible-conditional-polyfill/">previous blogpost</a> was about how to load a <code>:focus-visible</code> polyfill only if the requesting browser doesn't support it. Similar to that, this snippet will help you to load an image lazyloading JavScript library, only when <a href="https://developer.mozilla.org/en-US/docs/Web/HTML/Element/img#attr-loading">native lazyloading</a> isn't supported.</p>
<h2>Intro to lazyloading</h2>
<p>Lazyloading images has been a good practice for web page performance for some time, and recommended by tools like <a href="https://web.dev/measure/">Lighthouse</a>, <a href="https://developers.google.com/speed/pagespeed/insights/">PageSpeed Insights</a> and <a href="https://www.webpagetest.org/">WebPageTest</a> among others. This traditionally had to implemented using a JS library like <a href="https://github.com/aFarkas/lazysizes">Lazysizes</a>.</p>
<p>These libraries monitor what is visible within the browser and only when an image is about to come into view is it loaded. This means that browser won't need to download any images that are never seen - reducing data use and potentially improving front-end performance.</p>
<h2>Native lazyloading</h2>
<p>Given the prevelance of this practice, the Chrome team and HTML Spec folk introduced lazyloading behaviour natively into the browser via the <code>loading</code> attribute on <code>img</code> tags. We can already make our current <code>img</code> tags lazy by adding <code>loading=&quot;lazy&quot;</code> to the element like so:</p>
<pre class="language-html"><code class="language-html"><span class="token tag"><span class="token tag"><span class="token punctuation">&lt;</span>img</span> <span class="token attr-name">src</span><span class="token attr-value"><span class="token punctuation attr-equals">=</span><span class="token punctuation">"</span>/assets/example.jpg<span class="token punctuation">"</span></span> <span class="token attr-name">alt</span><span class="token attr-value"><span class="token punctuation attr-equals">=</span><span class="token punctuation">"</span>Example image<span class="token punctuation">"</span></span> <span class="token attr-name">width</span><span class="token attr-value"><span class="token punctuation attr-equals">=</span><span class="token punctuation">"</span>200<span class="token punctuation">"</span></span> <span class="token attr-name">height</span><span class="token attr-value"><span class="token punctuation attr-equals">=</span><span class="token punctuation">"</span>100<span class="token punctuation">"</span></span> <span class="token attr-name">loading</span><span class="token attr-value"><span class="token punctuation attr-equals">=</span><span class="token punctuation">"</span>lazy<span class="token punctuation">"</span></span><span class="token punctuation">></span></span></code></pre>
<p><a href="https://caniuse.com/loading-lazy-attr">Browser support</a> is decent at around 70% between Chromium-based and Firefox-based browsers, but it unfortunately isn't yet in Safari or for iOS at all.</p>
<p>As with my focus-visible conditional loading, ideally we load a JavaScript library/polyfill only if the new feature isn't supported.</p>
<h2>Loading the library conditionally</h2>
<p>The progressive nature of the <code>loading</code> attribute means older browsers without support will still load the images. That is normally great as it keeps the web backwards-compatible and often usable in old browsers and devices. In this case however, it makes it a little tricky for us to prevent the loading of images outside of the current view.</p>
<p>Browsers that don't support the attribute ignore it and will just load the images normally. By the time we've loaded our script, the browser may have already downloaded many or all of the images on the page unnecessarily.</p>
<p>What we have to do is provide our markup in the format of the lazyload library we are using. We then check for support of native lazyloading and either load our library or run some JS to adapt our markup to 'normal'.</p>
<p>Before the closing <code>&lt;/body&gt;</code> we include our conditional loading snippet like this:</p>
<pre class="language-html"><code class="language-html"><span class="token tag"><span class="token tag"><span class="token punctuation">&lt;</span>script</span><span class="token punctuation">></span></span><span class="token script"><span class="token language-javascript"><br>  <span class="token keyword">let</span> hasLibLoaded <span class="token operator">=</span> <span class="token boolean">false</span><span class="token punctuation">;</span><br>  <span class="token comment">// in a function so we cn re-run if data is added dynamically</span><br>  window<span class="token punctuation">.</span><span class="token function-variable function">loadingPolyfill</span> <span class="token operator">=</span> <span class="token punctuation">(</span><span class="token punctuation">)</span> <span class="token operator">=></span> <span class="token punctuation">{</span><br>    <span class="token comment">// check if loading attribute supported</span><br>    <span class="token keyword">if</span> <span class="token punctuation">(</span><span class="token string">'loading'</span> <span class="token keyword">in</span> <span class="token class-name">HTMLImageElement</span><span class="token punctuation">.</span>prototype<span class="token punctuation">)</span> <span class="token punctuation">{</span><br>      <span class="token comment">// get all &lt;img> and &lt;source> elements</span><br>      <span class="token keyword">const</span> images <span class="token operator">=</span> document<span class="token punctuation">.</span><span class="token function">querySelectorAll</span><span class="token punctuation">(</span><span class="token string">'img[data-src]'</span><span class="token punctuation">)</span><span class="token punctuation">;</span><br>      <span class="token keyword">const</span> sources <span class="token operator">=</span> document<span class="token punctuation">.</span><span class="token function">querySelectorAll</span><span class="token punctuation">(</span><span class="token string">'source[data-srcset]'</span><span class="token punctuation">)</span><span class="token punctuation">;</span><br><br>      <span class="token comment">// loop through &lt;img>s setting the src attribute and srcset and sizes if present</span><br>      <span class="token keyword">for</span> <span class="token punctuation">(</span><span class="token keyword">let</span> img <span class="token keyword">of</span> images<span class="token punctuation">)</span> <span class="token punctuation">{</span><br>        img<span class="token punctuation">.</span>src <span class="token operator">=</span> img<span class="token punctuation">.</span><span class="token function">getAttribute</span><span class="token punctuation">(</span><span class="token string">'data-src'</span><span class="token punctuation">)</span><span class="token punctuation">;</span><br>        <span class="token keyword">const</span> srcset <span class="token operator">=</span> img<span class="token punctuation">.</span><span class="token function">getAttribute</span><span class="token punctuation">(</span><span class="token string">'data-srcset'</span><span class="token punctuation">)</span><span class="token punctuation">;</span><br>        <span class="token keyword">if</span> <span class="token punctuation">(</span>srcset<span class="token punctuation">)</span> <span class="token punctuation">{</span><br>          img<span class="token punctuation">.</span>srcset <span class="token operator">=</span> srcset<span class="token punctuation">;</span><br>        <span class="token punctuation">}</span><br>        <span class="token keyword">const</span> sizes <span class="token operator">=</span> img<span class="token punctuation">.</span><span class="token function">getAttribute</span><span class="token punctuation">(</span><span class="token string">'data-sizes'</span><span class="token punctuation">)</span><span class="token punctuation">;</span><br>        <span class="token keyword">if</span> <span class="token punctuation">(</span>sizes<span class="token punctuation">)</span> <span class="token punctuation">{</span><br>          img<span class="token punctuation">.</span>sizes <span class="token operator">=</span> sizes<span class="token punctuation">;</span><br>        <span class="token punctuation">}</span><br>      <span class="token punctuation">}</span><br><br>      <span class="token comment">// loop through &lt;source>s setting the srcset attribute and sizes if present</span><br>      <span class="token keyword">for</span> <span class="token punctuation">(</span><span class="token keyword">let</span> source <span class="token keyword">of</span> sources<span class="token punctuation">)</span> <span class="token punctuation">{</span><br>        source<span class="token punctuation">.</span>srcset <span class="token operator">=</span> source<span class="token punctuation">.</span><span class="token function">getAttribute</span><span class="token punctuation">(</span><span class="token string">'data-srcset'</span><span class="token punctuation">)</span><span class="token punctuation">;</span><br>        <span class="token keyword">const</span> sizes <span class="token operator">=</span> source<span class="token punctuation">.</span><span class="token function">getAttribute</span><span class="token punctuation">(</span><span class="token string">'data-sizes'</span><span class="token punctuation">)</span><span class="token punctuation">;</span><br>        <span class="token keyword">if</span> <span class="token punctuation">(</span>sizes<span class="token punctuation">)</span> <span class="token punctuation">{</span><br>          source<span class="token punctuation">.</span>sizes <span class="token operator">=</span> sizes<br>        <span class="token punctuation">}</span><br>      <span class="token punctuation">}</span><br><br>    <span class="token comment">// if loading attribute is not supported</span><br>    <span class="token punctuation">}</span> <span class="token keyword">else</span> <span class="token punctuation">{</span><br>      <span class="token comment">// check we haven't already loaded the library</span><br>      <span class="token keyword">if</span> <span class="token punctuation">(</span><span class="token operator">!</span>hasLibLoaded<span class="token punctuation">)</span> <span class="token punctuation">{</span><br>        <span class="token comment">// create script element with src pointing to our library and add to document</span><br>        <span class="token keyword">const</span> script <span class="token operator">=</span> document<span class="token punctuation">.</span><span class="token function">createElement</span><span class="token punctuation">(</span><span class="token string">'script'</span><span class="token punctuation">)</span><span class="token punctuation">;</span><br>        script<span class="token punctuation">.</span>src <span class="token operator">=</span> <span class="token string">'/js/lazysizes.js'</span><span class="token punctuation">;</span><br>        document<span class="token punctuation">.</span>body<span class="token punctuation">.</span><span class="token function">appendChild</span><span class="token punctuation">(</span>script<span class="token punctuation">)</span><span class="token punctuation">;</span><br><br>        <span class="token comment">// mark library as loaded</span><br>        hasLibLoaded <span class="token operator">=</span> <span class="token boolean">true</span><span class="token punctuation">;</span><br><br>      <span class="token comment">// lazyloading library has already been loaded</span><br>      <span class="token punctuation">}</span> <span class="token keyword">else</span> <span class="token punctuation">{</span><br>        <span class="token comment">// depending on your library you may need to run findNewItems() or something along</span><br>        <span class="token comment">// those lines to adapt new content. Some libraries including lazysizes don't need this.</span><br>      <span class="token punctuation">}</span><br>    <span class="token punctuation">}</span><br>  <span class="token punctuation">}</span><br>  <span class="token comment">// run our loading polyfill</span><br>  window<span class="token punctuation">.</span><span class="token function">loadingPolyfill</span><span class="token punctuation">(</span><span class="token punctuation">)</span><span class="token punctuation">;</span><br></span></span><span class="token tag"><span class="token tag"><span class="token punctuation">&lt;/</span>script</span><span class="token punctuation">></span></span></code></pre>
<p>We assign our function globally on the <code>window</code> object so that if any content is loaded via JavaScript (eg AJAX or client-side-routing) you call call <code>window.loadingPolyfill()</code> again and it will re-run including new images.</p>
<h2>Notes</h2>
<ul>
<li>Make sure the <code>script.src</code> points to your JS library - locall or using a CDN like <a href="https://www.jsdelivr.com/package/npm/lazysizes">JSDelivr</a>.</li>
<li>Depending on your lazyloading library, you may need to change <code>data-src</code>, <code>data-srcset</code> and <code>data-sizes</code>. Many use this convention but not all, eg <a href="https://github.com/hannahwoodward/uncloak">Uncloak</a> uses <code>data-uncloak-src</code>.</li>
<li>If you need to support older browsers like IE11 then you will need to check your lazyload library and adapt the code above. Alternatively consider including a <code>legacy.js</code> script that has the same functionality as our supporting case, that will fall back to standard image loading for old browsers.</li>
</ul>
<h2>Performance Impact</h2>
<p>Despite minimal, this will have a performance impact on both supporting and non-supporting browsers.</p>
<h3>Supporting browsers</h3>
<p>In theory browsers are able to start downloading high-priority images before the full document is parsed. Because there is no <code>src</code> atribute, our solution stops this from happening until our script runs near the end of the document. Unless you have a very long HTML document though, it's unlikely this will be more than a few milliseconds. Regardless, I would suggest avoiding this practice for your most important above-the-fold images like logos or hero images.</p>
<h3>Non-supporting browsers</h3>
<p>As we are loading our JS library asyncronously, this generally means it has a lower download priority than it would otherwise. There is no easy way around this, but I couldn't see any conslusive impact when testing on Safari. Take that with a pinch of salt though, it will depend a lot on how your website is built and the visiting device. I don't think this will be very significant however.</p>
<h2>Further Reading</h2>
<ul>
<li><a href="https://developer.mozilla.org/en-US/docs/Web/HTML/Element/img#attr-loading">loading attribute on MDN (developer.mozilla.org)</a></li>
<li><a href="https://web.dev/browser-level-image-lazy-loading/">web.dev lazy loading article (web.dev)</a></li>
<li><a href="https://github.com/aFarkas/lazysizes">lazysizes JS library (github.com)</a></li>
<li><a href="/writing/focus-visible-conditional-polyfill/">focus-visible polyfill (alistairshepherd.uk)</a></li>
<li>The <a href="https://github.com/mfranzke/loading-attribute-polyfill">loading attribute polyfill (github.com)</a> should work with some modifications to the code above, although I haven't used it before.</li>
</ul>

      <hr>
      <p>If you have any comments or feedback on this article, let me know! I'd love to hear your thoughts, go ahead and <a href="mailto:alistair@accudio.com">send me an email</a> at alistair@accudio.com or <a href="https://front-end.social/@accudio" rel="noopener noreferrer">contact me on Mastodon</a>.</p>
      <p>The post <a href="https://www.alistairshepherd.uk/writing/image-lazyload-conditional-polyfill/">Conditionally loading a native image lazyload polyfill/library</a> appeared first on <a href="https://alistairshepherd.uk">alistairshepherd.uk</a>.</p>
    ]]>
      </content>
    </entry>
  
    
    <entry>
      <title>Front End Web Development Feeds and Newsletters</title>
      <link href="https://www.alistairshepherd.uk/writing/frontend-feeds-newsletters/"/>
      <updated>2021-07-15T00:00:00Z</updated>
      <id>https://www.alistairshepherd.uk/writing/frontend-feeds-newsletters/</id>
      <content type="html">
        <![CDATA[
      <p>The world of Web Development is exciting and moves fast - particularly front end development in the past few years.</p>
<p>Although all of this innovation is great, it can sometimes be pretty difficult to keep up with. One of the ways I keep on top of the latest changes, trends and new technologies is by following the blogs and newsletters of companies and developers leading the industry.</p>
<p>I'm always on the lookout for more blogs or newsletters! If you have any suggestions I should check out <a href="#contact">get in touch</a>!</p>
<h2>Table of Contents</h2>
<ol>
<li><a href="#intro">Intro</a></li>
<li><a href="#browsers">Browser Vendors</a></li>
<li><a href="#companies">Company Blogs</a></li>
<li><a href="#dev-blogs">Development Blogs</a></li>
<li><a href="#developers">Developers and Designers</a></li>
<li><a href="#magazines">Magazines</a></li>
<li><a href="#newsletters">Newsletters</a></li>
</ol>
<h2 id="intro" tabindex="-1">Intro</h2>
<p>I thought I'd crack out a post I can point people to with a list of the feeds and newsletters I subscribe to, with a bit of hopefully useful info so you know if you want to subscribe yourself. Most of these are focused on:</p>
<ul>
<li>Front End Development</li>
<li>Full-Stack development</li>
<li>HTML</li>
<li>CSS</li>
<li>JavaScript</li>
<li>Accessibility</li>
<li>Web Performance</li>
<li>Browsers</li>
<li>11ty</li>
</ul>
<p>I find it quite fun that the format for the majority of these feeds about modern web development is RSS, a technology that has existed since 1999! If you're looking for a feed reader I use Feedbin, which is a great cloud RSS and newsletter reader across multiple platforms. You can also find an <a href="https://github.com/accudio/rss">export of my RSS subscriptions</a> on <a href="https://github.com/accudio">my Github</a>.</p>
<h2 id="browsers" tabindex="-1">Browser Vendors</h2>
<p>Useful for keeping up with changes to the biggest browsers. Topics can be quite technical and writing not particularly approachable, but probably worth a look. I tend to scan the titles to decide if it's something that will likely affect me before reading.</p>
<ul>
<li><a href="https://blog.google/products/chrome/">Google Chrome blog</a> [<a href="https://blog.google/products/chrome/rss/">feed</a>] — Fairly approachable summaries of changes, updates and new features coming to Google chrome and probably other chromium browsers;</li>
<li><a href="https://blog.chromium.org">Chromium Blog</a> [<a href="http://blog.chromium.org/atom.xml">feed</a>] — Similar to the Google Chrome blog but more technical, and posts summaries of every major version;</li>
<li><a href="https://hacks.mozilla.org">Mozilla Hacks</a> [<a href="https://hacks.mozilla.org/feed/">feed</a>] — Updates on Firefox and Gecko changes, mixture of technical level, summary of major updates;</li>
<li><a href="https://blogs.windows.com/msedgedev/">Microsoft Edge Blog</a> [<a href="https://blogs.windows.com/msedgedev/feed">feed</a>] — MS' work on Blink and Chromium, will eventually come to all Chromium browsers so good to follow;</li>
<li><a href="https://planet.webkit.org">Planet Webkit</a> [<a href="http://planet.webkit.org/atom.xml">feed</a>] — Very technical updates to WebKit and Safari - useful however due to a lack of official Safari updates. Keep in mind WebKit changes can take months or even years to make their way to Safari;</li>
<li><a href="https://planet.igalia.com/">Planet Igalia</a> [<a href="planet.igalia.com/rss20.xml">feed</a>] — The Igalia team have contributed to all browsers, most recently :focus-visible in Safari. Some really deep-dives into very technical topics. Not for everyone.</li>
</ul>
<h2 id="companies" tabindex="-1">Company Blogs</h2>
<p>Any blogs by development agencies or dev-related products. A few of the agency blogs have some great tips, best practices and open-source tools. I tend to be a bit wary of product blogs as they're always trying to sell but there are some decent ones out there.</p>
<ul>
<li><a href="https://9elements.com/blog/">9Elements</a> [<a href="https://9elements.com/blog/rss/">feed</a>] — Agency with some great front-end skills in their team. I particularly like Nils Binder's articles on layout like &quot;<a href="https://9elements.com/blog/the-devils-albatros-an-algorithmic-layout-technique/">The Devil's Albatross</a>&quot;;</li>
<li><a href="https://axesslab.com">Axess Lab</a> [<a href="https://axesslab.com/feed/">feed</a>] — Accessibility consultants, they have some great articles offering approachable summaries for accessibility topics including &quot;<a href="https://axesslab.com/what-is-a-screen-reader/">What is a screen reader?</a>&quot;;</li>
<li><a href="https://bejamas.io/blog/">Bejamas</a> [<a href="https://bejamas.io/blog/rss.xml">feed</a>] — An expert Jamstack agency, if you want more info on <a href="https://bejamas.io/discovery/static-site-generators/">Static Site Generators</a> or <a href="https://bejamas.io/discovery/headless-cms/">Headless CMS'</a> their articles are brilliant;</li>
<li><a href="https://www.debugbear.com/blog/">DebugBear</a> [<a href="https://www.debugbear.com/blog/feed/rss">feed</a>] — DebugBear sells web performance monitoring, I haven't used their product but they write some decent high-level articles about improving web performance.</li>
<li><a href="https://www.filamentgroup.com/lab/">Filament Group</a> [<a href="https://www.filamentgroup.com/lab/atom.xml">feed</a>] — If you've been on the web a while you've probably used a tool made by Filament Group! Authors of Respond.js, Picturefill, LoadCSS and <a href="https://www.filamentgroup.com/lab/load-css-simpler/">the most common async CSS pattern</a> - they do great open-source work and really interesting articles about them.</li>
<li><a href="https://volument.com/blog/">Volument Minimalism</a> [<a href="https://volument.com/rss.xml">feed</a>] — Privacy-focused analytics, haven't used the product but their insight on analytics and modern development is interesting and straight to the point.</li>
</ul>
<h2 id="dev-blogs" tabindex="-1">Development Blogs</h2>
<p>Blogs generally aimed at sharing knowledge about web development. Separate from Developers and Magazines as they feel somewhere between the two.</p>
<ul>
<li>CSS { In Real Life } [<a href="https://css-irl.info/rss.xml">feed</a>] — Generally short posts focused on CSS and layout. &quot;<a href="https://css-irl.info/detecting-hover-capable-devices/">Detecting Hover-Capable Devices</a>&quot; is a great recent one to check out;</li>
<li><a href="https://joy.recurse.com">Joy of Computing</a> [<a href="https://joy.recurse.com/feed.atom">feed</a>] — More general that most other feeds here. Short daily posts about cool or interesting things going on in Computing, can be pretty fun too;</li>
<li><a href="https://piccalil.li">Piccalilli</a> [<a href="https://piccalil.li/feed.xml">feed</a>] — Piccalilli by Andy Bell has great tutorials and summaries on building modern front ends. Check out &quot;<a href="https://piccalil.li/blog/a-modern-css-reset/">Modern CSS Reset</a>&quot;. The <a href="https://piccalil.li/membership/">Piccalilli community membership</a> is also great if you're into that!</li>
<li><a href="https://quirksmode.org/blog/">QuirksMode Blog</a> [<a href="https://quirksmode.org/blog/atom.xml">feed</a>] — QuirksMode has been writing about the web since 2004, talking a lot about browsers and thinking about how the web as a whole should work. Great thinkpieces.</li>
<li><a href="https://web.dev/blog/">web.dev</a> [<a href="https://web.dev/feed.xml">feed</a>] — Posts mostly by Google Web Developers introducing and covering best practices and new features in the web. The <a href="https://web.dev/learn/">courses</a> on the site are also well worth checking out;</li>
<li><a href="https://webplatform.news">Web Platform News</a> [<a href="https://webplatform.news/feed.xml">feed</a>] — Provides short updates on anything new in the web, including bugs and announcements you might not find elsewhere. Only available as a screenshot unless you pay $2/month on <a href="https://www.patreon.com/join/webplatformnews">Patreon</a>, worth considering though as I've found it very useful.</li>
</ul>
<h2 id="developers" tabindex="-1">Developers and Designers</h2>
<p>These are individual devs and designers who are experts in their field. Not as frequent posts, but there are some real gems here. In alphabetical order for simplicity, and just a short summary as there's a few!</p>
<ul>
<li><a href="https://addyosmani.com/">Addy Osmani</a> [<a href="https://addyosmani.com/rss.xml">feed</a>] — Google developer focusing on performance. Author of great book &quot;Image Optimisation&quot;;</li>
<li><a href="https://adrianroselli.com">Adrian Roselli</a> [<a href="https://adrianroselli.com/feed">feed</a>] — One of the best writers on accessibility and does loads of research and testing of screen readers etc;</li>
<li><a href="http://ishadeed.com/">Ahmad Shadeed</a> [<a href="https://ishadeed.com/feed.xml">feed</a>] — Dev/Designer who is a master of CSS;</li>
<li><a href="https://bradfrost.com/">Brad Frost</a> [<a href="http://feeds.feedburner.com/brad-frosts-blog">feed</a>] — Mostly thoughts on the front end design industry and design systems;</li>
<li><a href="https://www.brucelawson.co.uk/">Bruce Lawson</a> [<a href="https://www.brucelawson.co.uk/feed/">feed</a>] — Web veteran with a lot of accessibility knowledge. His Reading Lists provide a great ~monthly list of great articles from around the web.</li>
<li><a href="https://bryanlrobinson.com/">Bryan Robinson</a> [<a href="https://bryanlrobinson.com/feed.xml">feed</a>] — 11ty wizard and devrel for Sanity - a decent-looking headless CMS;</li>
<li><a href="https://chenhuijing.com/">Chen Hui Jing</a> [<a href="https://chenhuijing.com/feed.xml">feed</a>] — Well written articles from all areas of web dev from devops to CSS debugging;</li>
<li><a href="https://christianheilmann.com/">Christian Heilmann</a> [<a href="https://christianheilmann.com/feed/">feed</a>] — Shorter posts and introductions to various front end topics;</li>
<li><a href="http://daverupert.com/">Dave Rupert</a> [<a href="https://daverupert.com/atom.xml">feed</a>] — co-host of podcast ShopTalk, most of the blog is more about life as a dev and productivity but has some great dev articles also;</li>
<li><a href="https://davidwalsh.name/">David Walsh</a> [<a href="https://davidwalsh.name/feed">feed</a>] — One of the longest running dev blogs, David has so many brilliant articles, many of which are short tips with exactly what you need;</li>
<li><a href="https://dmitripavlutin.com/">Dmitri Pavlutin</a> [<a href="https://dmitripavlutin.com/rss.xml">feed</a>] — I love Dmitri's introductions to new JavaScript and React features and patterns;</li>
<li><a href="https://csswizardry.com/">Harry Roberts/CSS Wizardry</a> [<a href="https://feeds.feedburner.com/csswizardry">feed</a>] — Master web performance consultant, great articles providing fairly easy and accessible performance tips and advice;</li>
<li><a href="https://heydonworks.com/">Heydon Pickering</a> [<a href="https://heydonworks.com/feed.xml">feed</a>] — Internet maverick, accessibility expert, design systems writer and author of the brilliant &quot;Inclusive Components&quot;;</li>
<li><a href="https://hiddedevries.nl/en/blog/">Hidde De Vries</a> [<a href="https://hiddedevries.nl/rss/full/">feed</a>] — Accessibility, HTML and CSS expert;</li>
<li><a href="https://jakearchibald.com/">Jake Archibald</a> [<a href="https://jakearchibald.com/posts.rss">feed</a>] — Google developer and co-host of HTTP 203, writes great articles on web performance, images and modern JavaScript;</li>
<li><a href="https://meiert.com/en/">Jens Oliver Meiert</a> [<a href="https://meiert.com/en/feed/">feed</a>] — What you didn't realise you didn't know about HTML. Check out &quot;HTML Concepts: Body-Ok&quot;;</li>
<li><a href="https://adactio.com">Jeremy Keith</a> [<a href="https://adactio.com/articles/rss">feed</a>] — Jeremy has been writing about the web since 2001 and always has interesting thoughts of development and the industry in general. Manages the great agency <a href="https://clearleft.com">Clearleft</a>.</li>
<li><a href="https://jeremy.codes/">Jeremy Wagner</a> [<a href="https://jeremy.codes/rss.xml">feed</a>] — Web performance consultant and fan of simple JavaScript;</li>
<li><a href="https://joshwcomeau.com/">Josh W Comeau</a> [<a href="https://joshwcomeau.com/rss.xml">feed</a>] — React developer, writes great articles about JavaScript and HTML/CSS from the perspective of a JS dev;</li>
<li><a href="https://leftalign.substack.com/">Kat Maddox</a> [<a href="https://leftalign.substack.com/feed/">feed</a>] — Internet anarchist and computer/programming comedian.</li>
<li><a href="https://lea.verou.me/">Lea Verou</a> [<a href="http://feeds.feedburner.com/leaverou">feed</a>] — Works on web standards with the W3C Technical Architecture Group. Blog focused on CSS and JS;</li>
<li><a href="https://www.industrialempathy.com/">Malte Ubl</a> [<a href="https://www.industrialempathy.com/feed/feed.xml">feed</a>] — Web developer and software designer, particularly good articles on web performance;</li>
<li><a href="https://matuzo.at/">Manuel Matuzovic</a> [<a href="https://www.matuzo.at/feed.xml">feed</a>] — Creator of HTMHell, general tips and snippets of HTML, CSS and on accessibility;</li>
<li><a href="https://marcus.io">Marcus Herrmann</a> [<a href="https://marcus.io/feed">feed</a>] — Accessibility specialist, writes lots of great articles helping you test for accessibility and digging into testing various practices.</li>
<li><a href="https://markosaric.com/">Marko Saric</a> [<a href="https://markosaric.com/feed/">feed</a>] — Articles on analytics, web privacy and the state of the web as a whole;</li>
<li><a href="https://mxb.dev/">Max Böck</a> [<a href="https://mxb.dev/feed.xml">feed</a>] — Creative front end developer and creator of the Whimsical Web Club;</li>
<li><a href="https://firt.dev/">Maximiliano Firtman</a> [<a href="https://firt.dev/feed/feed.xml">feed</a>] — Best known for being a PWA expert and a better source of Safari news than anyone who work on it. I particularly love his Image CDN article;</li>
<li><a href="https://nicolas-hoizey.com/">Nicolas Hoizey</a> [<a href="https://nicolas-hoizey.com/feeds/articles.xml">feed</a>] — 11ty plugin author and writes on front end developer topics. He also shares lots of great wee stuff on his &quot;Notes&quot;;</li>
<li><a href="http://rachelandrew.co.uk/archives">Rachel Andrew</a> [<a href="https://rachelandrew.co.uk/archives/rss.php">feed</a>] — One of the best technical writers /editors around, working for Smashing, MDN and web.dev. Brilliant writing about CSS Layout;</li>
<li><a href="https://smth.uk/">Sam Smith</a> [<a href="https://smth.uk/feed/articles.xml">feed</a>] — Great freelance dev, writes about anything relevant to modern web development;</li>
<li><a href="https://whistlr.info/">Sam Thorogood</a> [<a href="https://whistlr.info/rss.xml">feed</a>] — Technical articles about JS, HTML and computing;</li>
<li><a href="https://sarasoueidan.com/blog/">Sara Soueidan</a> [<a href="https://www.sarasoueidan.com/blog/index.xml">feed</a>] — Writes about Accessibility, HTML, CSS and SVG mostly but anything relevant to front end development;</li>
<li><a href="https://sia.codes">Sia Karamalegos</a> [<a href="https://sia.codes/feed/feed.xml">feed</a>] — Writes a lot about 11ty, but also more general Jamstack and web performance topics;</li>
<li><a href="https://blog.tomayac.com">Thomas Steiner</a> [<a href="https://blog.tomayac.com/feed/feed.xml">feed</a>] — Google developer working on PWAs and 'Project Fugu', blog is more general explanations of browser features and APIs.</li>
<li><a href="https://timkadlec.com/">Tim Kadlec</a> [<a href="https://timkadlec.com/atom.xml">feed</a>] — Now working at WebPageTest, great writing on web performance;</li>
<li><a href="https://tobiasahlin.com">Tobias Ahlin Bjerrome</a> [<a href="https://tobiasahlin.com/feed.xml">feed</a>] — Writes about JS, CSS and Design. Each post focuses on one a particular topic and explains it really well and thoroughly.</li>
<li><a href="https://www.zachleat.com/web/">Zach Leatherman</a> [<a href="https://www.zachleat.com/web/feed/">feed</a>] — Now working at Netlify, author of 11ty, Glyphhanger and has written some of the best Web Font loading articles around;</li>
<li><a href="https://zellwk.com/">Zell Liew</a> [<a href="https://zellwk.com/feed.xml">feed</a>] — Brilliant writer whose articles are super easy to understand. Covers so many useful topics including system setup, build chains, technical JavaScript and industry advice;</li>
</ul>
<h2 id="magazines" tabindex="-1">Magazines</h2>
<p>The big blogs that feature articles from many different authors. A few only run for a month a year, others are year-round. If you only want to subscribe to a few blogs, make it these ones.</p>
<h3>Year-round</h3>
<ul>
<li><a href="https://css-tricks.com">CSS Tricks</a> [<a href="https://css-tricks.com/feed/">feed</a>] — The big one, possibly the best source for web development articles out there;</li>
<li><a href="https://www.smashingmagazine.com">Smashing Magazine</a> [<a href="https://www.smashingmagazine.com/feed">feed</a>] — Generally goes into more detail than CSS Tricks, with longer and more in-depth articles;</li>
<li><a href="https://tympanus.net/codrops/">Codrops</a> [<a href="https://tympanus.net/codrops/feed/">feed</a>] — Publishes the 'Collective', a regular summary of great articles in web development. Also has some super cool JavaScript effect tutorials;</li>
</ul>
<h3>Seasonal</h3>
<ul>
<li><a href="https://www.24a11y.com">24 Accessibility</a> [<a href="https://www.24a11y.com/feed/">feed</a>] — Advent calendar of accessibility articles;</li>
<li><a href="https://24ways.org/">24 ways</a> [<a href="https://feeds.feedburner.com/24ways">feed</a>] — Advent calendar of cool/impressive  things you can do on the web;</li>
<li><a href="https://www.javascriptjanuary.com/">JavaScript January</a> [<a href="https://www.javascriptjanuary.com/blog?format=rss">feed</a>] — Features all sorts of JS-related articles throughout January, including vanilla, front end frameworks, node.js and testing;</li>
<li><a href="https://calendar.perfplanet.com">Web Performance Calendar</a> [<a href="https://calendar.perfplanet.com/feed/">feed</a>] — Advent calendar of front and back end web performance articles.</li>
</ul>
<h2 id="newsletters" tabindex="-1">Newsletters</h2>
<p>Some feed readers allow you to receive email newsletters in the same place, I used Feedbin for this. Regardless, these are some great newsletters to subscribe to:</p>
<ul>
<li><a href="https://www.calibreapp.com/newsletter">Calibre Performance Newsletter</a> — Regular summary of web performance articles;</li>
<li><a href="https://csslayout.news">CSS Layout News</a> — Curated by Rachel Andrew, this is a newsletter with news, articles and tips specifically focused on web page layouts with CSS;</li>
<li><a href="https://www2.fauna.com/l/517431/2018-07-09/6d1b8j">Fauna Developer Weekly</a> — Developer-focused tips and tricks. Fauna is a database provider so a bit more back end focused;</li>
<li><a href="https://frontendfoc.us/">Frontend Focus</a> — Weekly roundup of front end news and articles, also includes tutorials, jobs and tool suggestions;</li>
<li><a href="https://frontend.horse/">Frontend Horse</a> — focuses on modern CSS and really dynamic and animated interactions. Not super frequent but each issue goes into so much depth;</li>
<li><a href="https://frontendweekly.co/">Frontend Weekly</a> — Lots of articles about the JS ecosystem, particularly React tools and libraries;</li>
<li><a href="https://jamstack.email/">JAMstacked</a> — Jamstack news, articles, libraries and platforms;</li>
<li><a href="https://buttondown.email/cassidoo">Rendezvous with Cassidoo</a> — Weekly newsletter with a few links about the web, a few about other things, an interview question to try and a joke!</li>
<li><a href="https://uianimationnewsletter.com/">The UI Animation Newsletter</a> — A few links with super cool UI animations to check out;</li>
<li><a href="https://theanimatedweb.com/newsletter/">Tiny Web Animation</a> — a short email with 3 links to cool web animations;</li>
<li><a href="https://wdrl.info/">Web Development Reading List</a> — as the name suggests, a list of articles to check out about web development. Well curated;</li>
<li><a href="https://webtoolsweekly.com/">Web Tools weekly</a> — Weekly newsletter that focuses on tooling rather than articles. Links to libraries, packages, platforms and tools you can use while developing.</li>
</ul>

      <hr>
      <p>If you have any comments or feedback on this article, let me know! I'd love to hear your thoughts, go ahead and <a href="mailto:alistair@accudio.com">send me an email</a> at alistair@accudio.com or <a href="https://front-end.social/@accudio" rel="noopener noreferrer">contact me on Mastodon</a>.</p>
      <p>The post <a href="https://www.alistairshepherd.uk/writing/frontend-feeds-newsletters/">Front End Web Development Feeds and Newsletters</a> appeared first on <a href="https://alistairshepherd.uk">alistairshepherd.uk</a>.</p>
    ]]>
      </content>
    </entry>
  
    
    <entry>
      <title>Web Nostalgia and Lost In Translation</title>
      <link href="https://www.alistairshepherd.uk/writing/lost-in-translation-and-nostalgia/"/>
      <updated>2021-08-30T00:00:00Z</updated>
      <id>https://www.alistairshepherd.uk/writing/lost-in-translation-and-nostalgia/</id>
      <content type="html">
        <![CDATA[
      <p>When I was a kid growing up in the early 2000's, one of my favourite things was the family computer. And along with that a trusty booklet that came with one of my dad's PC magazines - &quot;100 great websites&quot;. I remember spending days and days visiting every website listed in the book many times over, taking notes about my favourites.</p>
<p>My first search engine experience came from this when Altavista was going to be the big player in web search (according to the book). The book had everything from the finest web portals, random neocities pages with all sorts of wacky graphics and the early web experiments that captured my imagination.</p>
<p>One of those experiments I remember the best was called &quot;Lost in Translation&quot;. Like most of the sites and links I remember it's long dead now, but at the time it was my favourite. Lost in Translation used the then-new Babel Fish Translation (later bought by Yahoo) API to take any input text, translate it through several different languages and return the resulting garbled mess back in English. I couldn't wait to get home after school, pretend I was doing schoolwork and instead try all sorts of sentences and phrases and see how they turned out.</p>
<p>Fast forward 20 years and I'm a web developer myself. It's almost certainly that booklet and those sites that started me on this journey and made me fall in love with the web. Lost in Translation is still something I think about every so often, an example of what the web was in those days and something I'd like a bit more of now.</p>
<p>Anyway, I was asked by some friends to make a quiz round. Inspired by Lost In Translation, I thought I'd make a quiz round where recipe titles were run through several languages and you had to guess which one. I'll be honest - the round was absolute fucking shite. The average score was 1/10 and the very best 4/10 - I had created questions so bad people did worse than random guesses would provide.</p>
<p>The result of it wasn't all bad though. When I was testing using the Google Translate UI I realised some recipes stood unchanged whilst some completely lost almost instantly. (Turns out that translating modern fad-based recipe names into little-known isolated languages doesn't work too well.) I realised I'd need to do this quite a lot, so wrote a small bit of JavaScript using the Google Translate API to make the process a bit easier.</p>
<p>There's nothing special about it, but having some code that replicates that functionality of Lost In Translation turns out to mean quite a lot to me. It's simple, only took me about an hour (most of which was trying to get a free translation API working) and has achieved it's purpose - but I'm unable to just let it go.</p>
<p>Putting it and these words on the internet pleases the archivist in me, the same part of me that is sad I no longer have that booklet and link rot made it practically useless anyway.</p>
<p>I was just trying to write a basic readme when this monologue came, but I guess now there's morals about link rot, web archival, nostalgia and wishes for a better web.</p>
<p>But enough of that, <a href="https://github.com/accudio/lost-in-translation">have some fucking code if you want it</a>.</p>

      <hr>
      <p>If you have any comments or feedback on this article, let me know! I'd love to hear your thoughts, go ahead and <a href="mailto:alistair@accudio.com">send me an email</a> at alistair@accudio.com or <a href="https://front-end.social/@accudio" rel="noopener noreferrer">contact me on Mastodon</a>.</p>
      <p>The post <a href="https://www.alistairshepherd.uk/writing/lost-in-translation-and-nostalgia/">Web Nostalgia and Lost In Translation</a> appeared first on <a href="https://alistairshepherd.uk">alistairshepherd.uk</a>.</p>
    ]]>
      </content>
    </entry>
  
    
    <entry>
      <title>Personal site stack for alistairshepherd.uk</title>
      <link href="https://www.alistairshepherd.uk/writing/personal-site-stack/"/>
      <updated>2021-12-23T00:00:00Z</updated>
      <id>https://www.alistairshepherd.uk/writing/personal-site-stack/</id>
      <content type="html">
        <![CDATA[
      <aside class="flow-space-800 gap-bot-800 rounded pad-400 bg-dark-m1 measure-long">
  <p class="gap-bot-400">I've had a very chaotic few months at the end of 2021, with work, moving house and my <a href="/speaking/jamstack-imagecdns/">first steps into giving tech talks</a>! That has meant that all the blog posts and side projects I planned were left behind, but with some free time before Christmas I thought I'd squeeze a quick and easy post out!</p>
  <p class="gap-bot-400">I hope 2021 has been kind and merciful to you, and you have a good holiday period if you're taking one! Thank you so much for reading and supporting me/my work, it means a huge amount! ❤️</p>
  <p>So I'm finishing off this year with a very festive post... my website tech stack?! 🌲</p>
</aside>
<p>In about a month this site will be a year old! Recently a few people have asked me about the tech stack so it's about time I put it and my thinking down properly. I'm really enjoying it!</p>
<p>The entire build is designed to make it as easy as possible for me to work on new content and tweaks after months or years of not touching it. It's been great so far and I find it easier to work on when I come back to it than other platforms I've worked on.</p>
<h2>Summary / Table of Contents</h2>
<p>Here's a TLDR if you're not interested in the reasoning:</p>
<ul>
<li><a href="#hosting">Host</a> — Netlify with Vercel backup ☁️</li>
<li><a href="#ssg">Static site generator</a> — Eleventy 🎈🐀</li>
<li><a href="#tasks">Tasks/build</a> — Gulp 🥤</li>
<li><a href="#css">CSS</a> — 'Vanilla' Scss with Gorko 🎨</li>
<li><a href="#js">JavaScript</a> — Vanilla JS with esbuild and Barba client-side routing ⚡</li>
<li><a href="#media">Media</a> — CloudImage Image CDN 🖼️</li>
<li><a href="#fonts">Fonts</a> — Red Hat Display and Literata, optimised with Glyphhanger 🕴️</li>
</ul>
<h2 id="hosting" tabindex="-1">Hosting</h2>
<p>For Jamstack hosting I really like <a href="https://www.netlify.com/">Netlify</a>. I think their product is brilliant and love how easy it makes deploying and hosting a website. It has tons of features, great documentation and I like their company ethos, principles and staff. My site is primarily hosted with them under the free plan.</p>
<p>However in case they have a major incident or I disagree with their direction, I have a backup copy of my site ready to go with <a href="https://vercel.com/">Vercel</a>. If I needed to switch to them, all I'd need to do is update the DNS and it would be done within a few hours if needed. I don't anticipate needing to, but when it's so easy and free to have a backup website I like having the option.</p>
<h2 id="ssg" tabindex="-1">Static Site Generator</h2>
<p>I use <a href="https://www.11ty.dev/">Eleventy</a> as my Static Site Generator (SSG) for data manipulation and HTML generation. My site is fairly simple, all I really need is handlebars-style templating, markdown support and reusable JS snippets for custom functionality.</p>
<p>This would give me a lot of SSG options but I had a few priorities that were important. I wanted static HTML without any client-side JavaScript, flexibility with data and structure, easily extendable with JavaScript, and to be in full control of the output. Eleventy was the ideal tool for the job here.</p>
<p>At the time of creation it was a year from stable release but already provided a quick, extendable platform that is easy to work on and has been more stable than at least 4 other major SSGs I've worked with!</p>
<p>I write my blog posts in Markdown but the rest of the site uses <a href="https://mozilla.github.io/nunjucks/">Nunjucks</a> for templating.</p>
<h2 id="tasks" tabindex="-1">Tasks/build</h2>
<p>I use <a href="https://gulpjs.com/">gulp</a> for my build process and tasks as it provides me a lot of freedom with how I want to run tasks and implement builds.</p>
<p>Many people consider gulp to be dated/dead but honestly I much prefer it to many of the big 'build tools' used in development at the moment. Webpack, Rollup and Parcel seem great at first but I've had difficulties with configuration or needed to use gulp alongside them for custom processes.</p>
<p>A year on I would consider simplifying further and using simple node scripts instead of gulp. These would have the benefits of simplicity and stability over time—I would also appreciate fewer dependencies. For getting off the ground quickly though gulp has the edge for me, with a still huge ecosystem and so many previous projects I can pull gulp tasks from.</p>
<p>A key feature is I can write my own tasks in JS and don't need to prescribe to a config system. The tasks for this site are fairly standard but for long-term maintenance it's useful being able to write my own without having to learn a new 'plugin' syntax. I can also implement builds using whatever tools I like—an example being <a href="#js">choosing to use esbuild for JS bundles</a>.</p>
<p>It's not as fancy as some of the latest tools like Vite and Snowpack, but in reality I don't need HMR or instant refreshes for a simple site. And although it's not at the cutting-edge, the API and project stability is helpful for coming back to an older project.</p>
<h2 id="css" tabindex="-1">CSS</h2>
<p>The CSS is mostly handcrafted without any libraries so I can have full control over the structure and performance. I write in <a href="https://sass-lang.com/">Sass (Scss flavour)</a> as I'm used to many of the utilities and conveniences it provides like importing partials, concatenated nesting and variables.</p>
<p>I say 'mostly handcrafted' as I use the utility class generator <a href="https://github.com/hankchizljaw/gorko">Gorko</a> to generate classes for spacing, sizing and colours. Utility classes are great for simple rules like changing <code>display</code> or spacing, both for convenience and performance reasons. For anything that has more than a couple utility classes though that becomes a 'layout' or 'block' that is written using BEM-like classes (eg <code>.nav__link</code>).</p>
<p>The structure I follow is a variation of Andy Bell's <a href="https://cube.fyi/">CUBE CSS</a> with Layout, Utilities and Blocks. For performance optimisation I have a 'critical' CSS file that includes CSS important to display the top of pages correctly, and this is embedded in the <code>head</code> of every page. I use an 11ty transform with <a href="https://github.com/FullHuman/purgecss">PurgeCSS</a> to strip out any unused rules for each page. This makes the first load of each page as fast as possible, and then the rest of the styles are loaded in a 'main' CSS file that is there for lower down the page and cached for subsequent navigations.</p>
<h2 id="js" tabindex="-1">JavaScript</h2>
<p>As I prioritise performance, almost all JavaScript is hand-written and vanilla — no frameworks. This allows me to include only what is needed, which thanks to the capabilities of modern browsers ends up being pretty small. Not including libraries and frameworks improves performance on all devices and means my code is more maintainable in future.</p>
<p>I really like <a href="https://github.com/evanw/esbuild">esbuild</a> for transforming source JS, it's extremely fast, very simple and the <a href="https://github.com/ym-project/gulp-esbuild">gulp-esbuild</a> plugin is easy to use. I have it set up to turn a set of modules into a single bundle for performance reasons, minify for production, generate sourcemaps and transform modern syntax to a list of supported browsers.</p>
<p>The only library I use is <a href="https://barba.js.org/">Barba</a> for Client Side Routing (CSR) to maintain the state of the landscape and themes seamlessly across pages. Although I don't normally care much for page transitions and client-side navigations, I couldn't come up with a native solution that was quick, wasn't jarring and maintained the effect. I broke my 'no libraries' rule here as client-side routing isn't easy to get right. Barba does a decent job, is fairly small and I load it separate from the main bundle with low priority to avoid a performance hit.</p>
<h2 id="media" tabindex="-1">Media</h2>
<p>I'm a big proponent of using Image CDNs, it makes the build process simpler and quicker and further development easier. For more of my thoughts see my recent talk <a href="/speaking/jamstack-imagecdns/">Making Assets fly on the Jamstack with Image CDNs</a>.</p>
<p>I use <a href="https://www.cloudimage.io">CloudImage</a> for this site as I like it's simplicity and the free tier is generous enough to cover my few images. The performance is good but I'd like to see better, including AVIF support. <a href="https://imgix.com/">Imgix</a> and <a href="https://cloudinary.com/">Cloudinary</a> both perform better but I'm happy with CloudImage for the moment.</p>
<p>I've written a custom Eleventy shortcode with a few parameters to generate <code>src</code> and <code>srcset</code> attributes to do what I need. This would make it easy to switch to a different provider if I wanted. To avoid the performance impact of using a different origin I <a href="https://github.com/Accudio/alistair-shepherd/blob/92dbe295c402e4645ee463dc3e762fddfd673420/netlify.toml#L19">proxy CloudImage requests through Netlify using redirects</a>.</p>
<h2 id="fonts" tabindex="-1">Fonts</h2>
<p>I use <a href="https://fonts.google.com/specimen/Red+Hat+Display">Red Hat Display</a> for titles and <a href="https://fonts.google.com/specimen/Literata">Literata</a> for the body. Both are on Google Fonts and open source so I can download and manipulate them freely.</p>
<p>To mitigate the performance effect of custom fonts I host them myself and preload the font files. I also reduce their size by subsetting them to US ASCII characters using <a href="https://github.com/zachleat/glyphhanger">Glyphhanger</a> which cuts their size almost in half. Thanks to <a href="https://twitter.com/hankchizljaw">Andy Bell</a> for the font combo!</p>
<hr class="flow-space-900 gap-bot-900">
<h2 id="conclusion" tabindex="-1">Conclusion</h2>
<p>I'm really happy with this stack and I'm hopeful it'll stand the test of time. My previous Next.js personal site was an absolute nightmare of dependency updates a year on from launching so we're doing better than that.</p>
<p>If you're interested in any specific implementations the <a href="https://github.com/Accudio/alistair-shepherd">source code is public on GitHub</a>. Note that it's not open-source and licensed for re-use, but if you're looking for a similar setup I'd encourage taking a look and learning from the code!</p>
<p>Whilst you're here check out my other posts about how I built this site, about the dynamic functionality of the landscape and colour themes:</p>
<ul>
<li><a href="/writing/parallax-svg-landscape-1/">Making a Parallax SVG Landscape - new site part 1</a></li>
<li><a href="/writing/parallax-svg-landscape-2/">SVG Landscape with live colour theming - new site part 2</a></li>
</ul>
<p>Thank you for reading, best wishes to you and yours, and take care!</p>

      <hr>
      <p>If you have any comments or feedback on this article, let me know! I'd love to hear your thoughts, go ahead and <a href="mailto:alistair@accudio.com">send me an email</a> at alistair@accudio.com or <a href="https://front-end.social/@accudio" rel="noopener noreferrer">contact me on Mastodon</a>.</p>
      <p>The post <a href="https://www.alistairshepherd.uk/writing/personal-site-stack/">Personal site stack for alistairshepherd.uk</a> appeared first on <a href="https://alistairshepherd.uk">alistairshepherd.uk</a>.</p>
    ]]>
      </content>
    </entry>
  
    
    <entry>
      <title>Managing tracking consent with Cead Consent</title>
      <link href="https://www.alistairshepherd.uk/writing/cead-consent/"/>
      <updated>2022-04-10T00:00:00Z</updated>
      <id>https://www.alistairshepherd.uk/writing/cead-consent/</id>
      <content type="html">
        <![CDATA[
      <p>An issue many businesses and sites will have to deal with is cookie and tracking consent on their websites. The web however is plagued with a huge number of intrusive trackers, and terrible, frustrating and often illegal consent dialogs.</p>
<p>Many websites implement a notice that doesn't allow opt-out, some offer an option that does nothing, whilst others only offer an opt-out solution - conveniently after they've collected all of your data.</p>
<p>Cead (pronounced kee-yed) is a cookie and tracking consent manager that is simple, lightweight, easy to implement and free. It's designed to help you implement a simple Accept or Deny dialog that will actually enable or disable tracking.</p>
<p>Cead is primarily created in response an increase in unsolicited web surveillance, but also to assist with meeting the standards of regulation including the EU GDPR &amp; ePrivacy and California's CCPA. As privacy legislation becomes more strict it's important that solutions offer compliant opt-in and opt-out controls which Cead offers at it's core.</p>
<h2>Table of Contents</h2>
<ol>
<li><a href="#the-problem">The problem</a></li>
<li><a href="#possible-fix">A possible fix?</a></li>
<li><a href="#cead-consent">Cead Consent</a>
<ol>
<li><a href="#google-tag-manager">Using Cead Consent to manage Google Tag Manager</a></li>
</ol>
</li>
<li><a href="#conclusion">Conclusion</a></li>
</ol>
<h2 id="the-problem" tabindex="-1">The problem</h2>
<p>Tracking on the web has long been a difficult topic. The interests of business owners, SEO teams, Ad vendors, site users and lawmakers become almost impossible to resolve and frequently ignore each other.</p>
<p>I'm of the opinion that a site should have no tracking. This site has no analytics or anything, because your browsing is your own business. Check out <a href="https://css-tricks.com/aint-no-party-like-a-third-party/">Jeremy Keith's &quot;Ain’t No Party Like a Third Party&quot;</a> for his insight on third-party scripts.</p>
<p>I find however that is an impossible stance to maintain when building sites for other people. They are often used to tracking metrics to evaluate their success, generate leads or target their services.</p>
<p>I've worked in agencies where I've seen and worked on a lot of websites for a variety of clients. They vary in purpose, build, location, size and much more, but one thing almost all have in common is they handle tracking terribly.</p>
<p>This may be familiar to you, but if not let me demonstrate the situation. We build a site for a client and add Google Analytics to it - pretty standard. Google Analytics has an easy way to allow people to opt-out by setting a global variable so we integrate a wee popup that allows the user to opt out.</p>
<p>That works great until the client gets an SEO expert who wants to track conversions better. They ask you to add a couple more scripts and you dutifully do so, but these have no way to opt out so all you can do is add them.</p>
<p>Later on, they want to add more scripts so they either ask for a text box to add them arbitrarily, install plugins, or install a Tag Manager.</p>
<p>Before long, the site has 5 analytics scripts, 10 conversion trackers and a screen recorder. These may not respect the user's privacy settings or have a way to opt out, and the website could slow to a crawl.</p>
<p>Some developers will give up at the beginning of this process and instead of asking consent put a message saying &quot;This site uses cookies and tracks you. Deal with it or fuck off&quot;.</p>
<h3>Why this is an issue</h3>
<p>There are two reasons why this is a problem. Ethical and legal.</p>
<p>Ethically, if this is your site you are stalking your users - standing 2 metres behind them as they peruse your store. The level of what is acceptable here can be debated, but tracking someones every move without their ability to consent to this is not justified. Place yourself in the shoes of someone who is being tracked across the web by several trackers, without any knowledge that potentially every interaction and details about their computer and location are being harvested and stored. It's hard to dispute in those circumstances.</p>
<p>This is also illegal in many jurisdictions. Consumer privacy laws like GDPR and ePrivacy in the EU, and CCPA and similar in American states requires some level of consent to web tracking. I'm not a lawyer so contact one for proper advice, but this gist is at minimum you <strong>need</strong> a way for users to be able to meaningfully opt out of tracking.</p>
<h3>The opt-out issue</h3>
<p>The big problem with the requirement to offer an opt out is that this is <strong>very</strong> hard to do.</p>
<p>As I mentioned earlier, some scripts like Google Analytics offer a method to opt out. This still isn't ideal as you're loading a tracking script and then checking if you're allowed to run it, but it at least gives you some control.</p>
<p>However that one of few tracking scripts I have come across that allows a way to opt out. Lots of other scripts will happily run as soon as they load, without regard for consequences. Even those that do have methods to opt-out, may be individual for each service and be a nightmare to manage.</p>
<p>Developers can deal with this by dynamically adding scripts under certain conditions, but clients will want to add their own and may not consider the consequences.</p>
<p>As developers we're left in a difficult position. Laws require that tracking can be opt-out, but we have no way to do so.</p>
<h2 id="possible-fix" tabindex="-1">A possible fix?</h2>
<p>The way to fix this is to be in control of all tracking scripts, and then load them ourselves in response to a consent status.</p>
<p>There are many solutions to do this as investors have monopolised on businesses grappling with the issue of tracking and consent.</p>
<p>Some large 'privacy-focused' corporations offer pricey 'hosted consent solutions' that supposedly solve all your problems. However when I load the site of one, my browser tells me it's blocked 14 trackers.<br>
If you've ever been annoyed by a cookie popup, it's probably a solution like this. A big annoying popup that makes opting out difficult and will send all your preferences to a tracking service to track your consent.</p>
<p>My opinion is that some of these companies are morally corrupt. Tracking the consent of users on a remote server is still tracking and they charge extortionate fees to fix a problem their own investors created.</p>
<p>I think the fix is a lot easier. Our webpage only runs tracking scripts when we say so. That's why I made Cead Consent.</p>
<h2 id="cead-consent" tabindex="-1">Cead Consent</h2>
<p>Cead Consent is a small library designed to solve the issue of tracking consent by controlling when scripts can run on the client-side. By making a tiny modification to tracking scripts we can load them on-demand in response to consent status.</p>
<p>It is designed to be extremely simple, easy to use and lightweight, and I'll give you a quick demo of how you would use it to solve the problem of consent.</p>
<p>Check out <a href="https://github.com/accudio/cead-consent">the GitHub repo</a> for full instructions on <a href="https://github.com/accudio/cead-consent#installation">installation</a> and <a href="https://github.com/accudio/cead-consent#managing-tracking-scripts-and-images">usage</a>.</p>
<h3 id="google-tag-manager-fix" tabindex="-1">Using Cead Consent to manage Google Tag Manager</h3>
<p>First we need to install Cead. It can either be loaded from a CDN or installed via <code>npm</code>, here I'll use the CDN to make it easier. We need to add a CSS file, a JavaScript file, and a little bit of HTML:</p>
<pre class="language-html"><code class="language-html"><span class="token tag"><span class="token tag"><span class="token punctuation">&lt;</span>html</span><span class="token punctuation">></span></span><br>  <span class="token tag"><span class="token tag"><span class="token punctuation">&lt;</span>head</span><span class="token punctuation">></span></span><br>    <span class="token tag"><span class="token tag"><span class="token punctuation">&lt;</span>link</span> <span class="token attr-name">rel</span><span class="token attr-value"><span class="token punctuation attr-equals">=</span><span class="token punctuation">"</span>stylesheet<span class="token punctuation">"</span></span> <span class="token attr-name">href</span><span class="token attr-value"><span class="token punctuation attr-equals">=</span><span class="token punctuation">"</span>https://cdn.jsdelivr.net/npm/cead-consent@1/dist/cead.css<span class="token punctuation">"</span></span><span class="token punctuation">></span></span><br>  <span class="token tag"><span class="token tag"><span class="token punctuation">&lt;/</span>head</span><span class="token punctuation">></span></span><br><br>  <span class="token tag"><span class="token tag"><span class="token punctuation">&lt;</span>body</span><span class="token punctuation">></span></span><br>      <span class="token tag"><span class="token tag"><span class="token punctuation">&lt;</span>p</span><span class="token punctuation">></span></span>Hi! Could we please enable some services and cookies to improve your experience and our website?<span class="token tag"><span class="token tag"><span class="token punctuation">&lt;/</span>p</span><span class="token punctuation">></span></span><br>      <span class="token tag"><span class="token tag"><span class="token punctuation">&lt;</span>div</span> <span class="token attr-name">class</span><span class="token attr-value"><span class="token punctuation attr-equals">=</span><span class="token punctuation">"</span>cead__btns<span class="token punctuation">"</span></span><span class="token punctuation">></span></span><br>        <span class="token tag"><span class="token tag"><span class="token punctuation">&lt;</span>button</span> <span class="token attr-name">class</span><span class="token attr-value"><span class="token punctuation attr-equals">=</span><span class="token punctuation">"</span>cead__btn cead__btn--decline<span class="token punctuation">"</span></span><span class="token punctuation">></span></span>No, thanks.<span class="token tag"><span class="token tag"><span class="token punctuation">&lt;/</span>button</span><span class="token punctuation">></span></span><br>        <span class="token tag"><span class="token tag"><span class="token punctuation">&lt;</span>button</span> <span class="token attr-name">class</span><span class="token attr-value"><span class="token punctuation attr-equals">=</span><span class="token punctuation">"</span>cead__btn cead__btn--accept<span class="token punctuation">"</span></span><span class="token punctuation">></span></span>Okay!<span class="token tag"><span class="token tag"><span class="token punctuation">&lt;/</span>button</span><span class="token punctuation">></span></span><br>      <span class="token tag"><span class="token tag"><span class="token punctuation">&lt;/</span>div</span><span class="token punctuation">></span></span><br>    <span class="token tag"><span class="token tag"><span class="token punctuation">&lt;/</span>div</span><span class="token punctuation">></span></span><br><br>	<span class="token tag"><span class="token tag"><span class="token punctuation">&lt;</span>main</span><span class="token punctuation">></span></span><span class="token comment">&lt;!-- your page content --></span><span class="token tag"><span class="token tag"><span class="token punctuation">&lt;/</span>main</span><span class="token punctuation">></span></span><br><br>    <span class="token tag"><span class="token tag"><span class="token punctuation">&lt;</span>script</span> <span class="token attr-name">src</span><span class="token attr-value"><span class="token punctuation attr-equals">=</span><span class="token punctuation">"</span>https://cdn.jsdelivr.net/npm/cead-consent@1/dist/browser.js<span class="token punctuation">"</span></span><span class="token punctuation">></span></span><span class="token script"></span><span class="token tag"><span class="token tag"><span class="token punctuation">&lt;/</span>script</span><span class="token punctuation">></span></span><br>  <span class="token tag"><span class="token tag"><span class="token punctuation">&lt;/</span>body</span><span class="token punctuation">></span></span><br><span class="token tag"><span class="token tag"><span class="token punctuation">&lt;/</span>html</span><span class="token punctuation">></span></span></code></pre>
<p>Although Cead consent can be used with all sorts of tracking scripts or pixels, I feel it's at it's best when combined with a tag manager like Google Tag Manager.</p>
<p>We manage tracking scripts (and images) by modifying their code slightly so they'll only run when Cead allows them to. When used with a Tag Manager the client or SEO teams can add as many scripts as they'd like to Google Tag Manager and we need to modify only one script for Cead.</p>
<p>When you copy your script from Google Tag Manager, it will look something like this (with a different GTM_MEASUREMENT_ID):</p>
<pre class="language-js"><code class="language-js"><span class="token operator">&lt;</span>script<span class="token operator">></span><br>dataLayer<span class="token operator">=</span><span class="token punctuation">[</span><span class="token punctuation">]</span><span class="token punctuation">;</span><br><span class="token punctuation">(</span><span class="token keyword">function</span><span class="token punctuation">(</span><span class="token parameter">w<span class="token punctuation">,</span>l</span><span class="token punctuation">)</span><span class="token punctuation">{</span>w<span class="token punctuation">[</span>l<span class="token punctuation">]</span><span class="token operator">=</span>w<span class="token punctuation">[</span>l<span class="token punctuation">]</span><span class="token operator">||</span><span class="token punctuation">[</span><span class="token punctuation">]</span><span class="token punctuation">;</span>w<span class="token punctuation">[</span>l<span class="token punctuation">]</span><span class="token punctuation">.</span><span class="token function">push</span><span class="token punctuation">(</span><span class="token punctuation">{</span><span class="token string-property property">'gtm.start'</span><span class="token operator">:</span><br><span class="token keyword">new</span> <span class="token class-name">Date</span><span class="token punctuation">(</span><span class="token punctuation">)</span><span class="token punctuation">.</span><span class="token function">getTime</span><span class="token punctuation">(</span><span class="token punctuation">)</span><span class="token punctuation">,</span><span class="token literal-property property">event</span><span class="token operator">:</span><span class="token string">'gtm.js'</span><span class="token punctuation">}</span><span class="token punctuation">)</span><span class="token punctuation">}</span><span class="token punctuation">)</span><span class="token punctuation">(</span>window<span class="token punctuation">,</span><span class="token string">'dataLayer'</span><span class="token punctuation">)</span><span class="token punctuation">;</span><br><span class="token operator">&lt;</span><span class="token operator">/</span>script<span class="token operator">></span><br><span class="token operator">&lt;</span>script <span class="token keyword">async</span> src<span class="token operator">=</span><span class="token string">"https://www.googletagmanager.com/gtm.js?id=GTM_MEASUREMENT_ID&amp;l=dataLayer"</span><span class="token operator">></span><span class="token operator">&lt;</span><span class="token operator">/</span>script<span class="token operator">></span></code></pre>
<p>See that last line, the <code>&lt;script async src=&quot;...&quot;&gt;</code>? All we need to do is change the <code>src</code> attribute to <code>data-src</code>, and add the <code>data-cead</code> attribute, like so:</p>
<pre class="language-js"><code class="language-js"><span class="token operator">&lt;</span>script<span class="token operator">></span><br>dataLayer<span class="token operator">=</span><span class="token punctuation">[</span><span class="token punctuation">]</span><span class="token punctuation">;</span><br><span class="token punctuation">(</span><span class="token keyword">function</span><span class="token punctuation">(</span><span class="token parameter">w<span class="token punctuation">,</span>l</span><span class="token punctuation">)</span><span class="token punctuation">{</span>w<span class="token punctuation">[</span>l<span class="token punctuation">]</span><span class="token operator">=</span>w<span class="token punctuation">[</span>l<span class="token punctuation">]</span><span class="token operator">||</span><span class="token punctuation">[</span><span class="token punctuation">]</span><span class="token punctuation">;</span>w<span class="token punctuation">[</span>l<span class="token punctuation">]</span><span class="token punctuation">.</span><span class="token function">push</span><span class="token punctuation">(</span><span class="token punctuation">{</span><span class="token string-property property">'gtm.start'</span><span class="token operator">:</span><br><span class="token keyword">new</span> <span class="token class-name">Date</span><span class="token punctuation">(</span><span class="token punctuation">)</span><span class="token punctuation">.</span><span class="token function">getTime</span><span class="token punctuation">(</span><span class="token punctuation">)</span><span class="token punctuation">,</span><span class="token literal-property property">event</span><span class="token operator">:</span><span class="token string">'gtm.js'</span><span class="token punctuation">}</span><span class="token punctuation">)</span><span class="token punctuation">}</span><span class="token punctuation">)</span><span class="token punctuation">(</span>window<span class="token punctuation">,</span><span class="token string">'dataLayer'</span><span class="token punctuation">)</span><span class="token punctuation">;</span><br><span class="token operator">&lt;</span><span class="token operator">/</span>script<span class="token operator">></span><br><span class="token operator">&lt;</span>script <span class="token keyword">async</span> data<span class="token operator">-</span>src<span class="token operator">=</span><span class="token string">"https://www.googletagmanager.com/gtm.js?id=GTM_MEASUREMENT_ID&amp;l=dataLayer"</span> data<span class="token operator">-</span>cead<span class="token operator">></span><span class="token operator">&lt;</span><span class="token operator">/</span>script<span class="token operator">></span></code></pre>
<p>And that's it! With the installation of Cead and that small change to the script tag we've made it so users can choose to consent to tracking or not and their choice is respected.</p>
<h2 id="conclusion" tabindex="-1">Conclusion</h2>
<p>Although the best situation is to avoid adding tracking to sites where possible, it often isn't possible. The best situation then is to use a lightweight, simple consent manager that won't frustrate users, will respect their consent choices and is free and open-source.</p>
<p>Cead has more options including managing <a href="https://github.com/accudio/cead-consent#inline-scripts">inline scripts</a>, <a href="https://github.com/accudio/cead-consent#image-pixels">tracking 'pixels'</a>, an <a href="https://github.com/accudio/cead-consent#options">'opt-out mode'</a>, <a href="https://github.com/accudio/cead-consent#managing-cookies">cookie removal</a> and more. Check out the documentation on the <a href="https://github.com/accudio/cead-consent">GitHub repo</a> to see all it can do!</p>

      <hr>
      <p>If you have any comments or feedback on this article, let me know! I'd love to hear your thoughts, go ahead and <a href="mailto:alistair@accudio.com">send me an email</a> at alistair@accudio.com or <a href="https://front-end.social/@accudio" rel="noopener noreferrer">contact me on Mastodon</a>.</p>
      <p>The post <a href="https://www.alistairshepherd.uk/writing/cead-consent/">Managing tracking consent with Cead Consent</a> appeared first on <a href="https://alistairshepherd.uk">alistairshepherd.uk</a>.</p>
    ]]>
      </content>
    </entry>
  
    
    <entry>
      <title>SVG generative mountain ridge dividers</title>
      <link href="https://www.alistairshepherd.uk/writing/svg-generative-ridges/"/>
      <updated>2022-04-21T00:00:00Z</updated>
      <id>https://www.alistairshepherd.uk/writing/svg-generative-ridges/</id>
      <content type="html">
        <![CDATA[
      <p>This is another post about the build of this website. Check out the other posts in the <a href="/writing/tag/my-site/">my site tag</a>.</p>
<p>Today, I wanted to write a little about the section dividers used on my site. These ones:</p>
<div class="gap-bot-300 flow-space-major" style="grid-column: 1/-1;">
<div class="relative bg-mid" style="min-height:5rem">
    <div class="b-divider color-mid" role="img" aria-hidden="true">
      <svg viewBox="0 0 2000 50" preserveAspectRatio="none" xmlns="http://www.w3.org/2000/svg"><path fill="currentColor" d="m0 24.427 15.625-.12 15.625.281 15.625.993L62.5 24.1l15.625 2.328 15.625-.608 15.625-1.56L125 25.472l15.625-1.998 15.625-.171 15.625 1.727 15.625-1.088 15.625 3.191 15.625.138 15.625 1.466L250 26.494l15.625-2.403 15.625 1.155 15.625 4.943 15.625-.094 15.625 2.42 15.625 1.405 15.625-.744L375 32.553l15.625-2.222 15.625 2.11 15.625 4.468 15.625-.264 15.625.828 15.625-1.47 15.625-3.385L500 34.015l15.625-.957 15.625-.185 15.625-.818 15.625-2.008 15.625-.504 15.625 4.686 15.625 1.176L625 32.051l15.625 2.181 15.625-2.364 15.625-1.61 15.625 2.45 15.625-1.493 15.625.427 15.625-.347L750 35.282l15.625-2.582 15.625-1.938 15.625-.291 15.625.072 15.625 1.127 15.625 1.325 15.625 1.013 15.625-.47 15.625-1.207 15.625 3.826 15.625-1.266 15.625.58 15.625 1.935 15.625-2.976 15.625-2.582L1000 29.605l15.625-.997 15.625 2.681 15.625.035 15.625-1.019 15.625.16 15.625 2.28 15.625 2.581L1125 33.112l15.625.702 15.625-.854 15.625.352 15.625 1.148 15.625.355 15.625-4.445 15.625 2.46 15.625-.603 15.625-4.077 15.625 1.164 15.625 2.667 15.625.642 15.625 1.284 15.625 1.078 15.625-3.102L1375 33.305l15.625-1.382 15.625-.25 15.625 1.654 15.625-.407 15.625.155 15.625-1.693 15.625-2.537L1500 29.939l15.625-3.458 15.625-.33 15.625 2.112 15.625-1.294 15.625.31 15.625 3.317 15.625 1.032 15.625-.22 15.625-3.025 15.625 1.393 15.625 3.814 15.625.881 15.625-.475 15.625-1.298 15.625-3.735L1750 30.065l15.625-3.127 15.625 1.052 15.625.052 15.625-.436 15.625 1.226 15.625-2.986 15.625-2.419 15.625-.574 15.625.119 15.625 3.1 15.625-.68 15.625-.55 15.625 2.457 15.625-2.318 15.625-.494L2000 21.99V50H0Z"/></svg>
    </div>
  </div>
<div class="relative">
    <div class="b-divider color-dark" role="img" aria-hidden="true">
      <svg viewBox="0 0 2000 50" preserveAspectRatio="none" xmlns="http://www.w3.org/2000/svg"><path fill="currentColor" d="m0 23.657 15.625 1.721 15.625 2.779 15.625 1.27L62.5 26.744l15.625-.344 15.625-.167 15.625-2.636 15.625.188 15.625 2.92 15.625-1.598 15.625-.858 15.625-4.033 15.625-2.79 15.625 2.003 15.625 1.262L250 16.991l15.625 1.477 15.625-1.948 15.625-.63 15.625 3.93 15.625-2.175 15.625.24 15.625.132L375 15.435l15.625-.065 15.625-.09 15.625 2.874 15.625.426 15.625.302 15.625 2.743 15.625-.721L500 19.976l15.625 2.283 15.625-.08 15.625-.756 15.625-2.928 15.625-1.378 15.625 1.55 15.625 2.877L625 20.18l15.625 3.24 15.625-.9 15.625 1.204 15.625.28 15.625-2.181 15.625 1.855 15.625 1.635L750 23l15.625-3.041 15.625 1.167 15.625.593 15.625.315 15.625 1.7 15.625.826 15.625-2.715L875 21.49l15.625 1.806 15.625 3.985 15.625 1.122 15.625-1.361 15.625.17 15.625-1.653 15.625 1.838 15.625.418 15.625-1.313 15.625 2.044 15.625-.1 15.625-3.191 15.625-.205 15.625 2.86 15.625 1.363 15.625.608 15.625 1.111 15.625-3.55 15.625 2.647 15.625 1.176 15.625-.279 15.625-.728 15.625-2.67L1250 25.523l15.625-1.905 15.625 2.413 15.625-3.024 15.625 1.717 15.625-2.342 15.625 2.432 15.625 1.514 15.625.561 15.625 2.597 15.625-1.067 15.625-3.155 15.625.364 15.625-2.42 15.625-1.921 15.625-.379 15.625.37 15.625-.89 15.625-3.357 15.625 1.975 15.625-1.672 15.625-.112 15.625 1.808 15.625 2.58 15.625-.201 15.625-.266 15.625-1.13 15.625-2.83 15.625 1.909 15.625.142 15.625 1.551 15.625 3.8 15.625-1.09 15.625-.38 15.625-.454 15.625 2.437 15.625.31 15.625-2.274 15.625 1.461 15.625 1.481L1875 29.963l15.625.18 15.625.42 15.625-3.447 15.625-.868 15.625-1.408 15.625 3.043 15.625 1.424L2000 27.599V50H0Z"/></svg>
    </div>
  </div>
</div>
<p>If you've done any game development they may seem familiar, they're nothing particularly new! They are however a neat thing you can do with SVG and I love those!</p>
<p>For people who just want to dig into a demo, here you go!</p>
<iframe class="codepen" height="380" style="width:100%;" scrolling="no" title="CodePen Embed" src="https://codepen.io/anon/embed/VwyRjaj?height=380&theme-id=dark&default-tab=result" frameborder="0" loading="lazy" allowtransparency="true" allowfullscreen="true"><p><a href="https://codepen.io/accudio/pen/VwyRjaj" target="_blank" rel="noopener">See the Pen</a></p></iframe>
<p>If you want more of an explanation then here we go!</p>
<h2>Idea: Interesting section dividers</h2>
<p>You may have noticed, but this website has a bit of a theme. Pat yourself on the back if you guessed that it's a mountain/landscape theme.</p>
<p>The header and colour changes were the entire basis for my new site, and the colour scheme was to be very simple but impactful. It felt fairly natural that the background of page section should vary between different colours within the theme to separate them. What didn't feel natural though was the hard straight line between them. I played around with curved lines, skewed them, added wobble, but none seemed to feel quite right.</p>
<p>At this point my sister suggested I use a mountain ridge, matching the style of the header. I initially produced a simple SVG manually and inserted it between each section.</p>
<p>I liked how this looked, but when two were visible on screen at once it looked a bit silly them being identical (recreation below).</p>
<img class="measure-long" src="/images/dividers-matching.jpg?width=820" srcset="/images/dividers-matching.jpg?width=400 400w, /images/dividers-matching.jpg?width=600 600w, /images/dividers-matching.jpg?width=800 800w, /images/dividers-matching.jpg?width=1077 1077w, /images/dividers-matching.jpg?width=1500 1500w, /images/dividers-matching.jpg?width=2154 2154w" sizes="(min-width: 52rem) 800px, calc(100vw - 2.66rem)" alt="Two matching hilly dividers, either side of content. Looks a bit weird" width="1396" height="523" loading="lazy">
<p>I didn't really want to manually create more, although it would have been a quick workaround it didn't feel like it was really a solution. My temporary solution was to manipulate the one I did have, using <code>transform</code> to flip, rotate or scale it so it looked slightly different each time.</p>
<img class="measure-long" src="/images/dividers-matching-fix.jpg?width=820" srcset="/images/dividers-matching-fix.jpg?width=400 400w, /images/dividers-matching-fix.jpg?width=600 600w, /images/dividers-matching-fix.jpg?width=800 800w, /images/dividers-matching-fix.jpg?width=1077 1077w, /images/dividers-matching-fix.jpg?width=1500 1500w, /images/dividers-matching-fix.jpg?width=2154 2154w" sizes="(min-width: 52rem) 800px, calc(100vw - 2.66rem)" alt="Two matching hilly dividers, either side of content. The top one has been flipped and rotated, but still looks a bit weird" width="1396" height="523" loading="lazy">
<p>It bugged me that my site just had the one ridge design, but I didn't really like any of the solutions I came up with.</p>
<h2>Terrain Generation</h2>
<p>Some time later, I read an article from the Joy of Computing newsletter about terrain generation in game development. I really like the Joy of Computing, although I don't have much time for keeping up with the wider programming industry, their newsletters are cool projects or posts about different areas I don't normally follow like Game Development, DevOps, Hardware or Networking to name a few.</p>
<p>Although the post was not really relevant to me, it made me realise that terrain generation was exactly what I needed! A method to create unique 'ridges' generated every time I needed a new divider.</p>
<h2>Working with points</h2>
<p>The output format was pretty easy, it had to be SVG. That way I could generate it ahead of time and embed it in the document and not need to rely on client-side JavaScript or outputting a large image file. For my use-case I basically needed a shape with variable top and cover the below area to match the background colour.</p>
<p>I needed a way to convert however I generate the points of the line to an SVG <code>path</code> format. My input array in most cases was in the format <code>[ [ x, y ], ... ]</code>, acting as a programmatic dot-to-dot. Turns out that although the <code>path</code> syntax seems a bit complex, when you're building it ends up making a lot of sense. SVG has different 'commands' which do certain things with a few parameteres. Check out the path syntax on MDN for them all, but we're mostly interested in <code>L</code> which draws a line to the specified absolute point. With a <code>viewBox</code> that matches our generation coordinate system we can convert it like so:</p>
<pre class="language-js"><code class="language-js"><span class="token comment">// convert points into SVG path</span><br><span class="token keyword">function</span> <span class="token function">convertPath</span><span class="token punctuation">(</span><span class="token parameter">width<span class="token punctuation">,</span> height<span class="token punctuation">,</span> points</span><span class="token punctuation">)</span> <span class="token punctuation">{</span><br>  <span class="token comment">// add first M (move) command to go to the first point</span><br>  <span class="token keyword">const</span> first <span class="token operator">=</span> points<span class="token punctuation">.</span><span class="token function">shift</span><span class="token punctuation">(</span><span class="token punctuation">)</span><br>  <span class="token keyword">let</span> path <span class="token operator">=</span> <span class="token template-string"><span class="token template-punctuation string">`</span><span class="token string">M </span><span class="token interpolation"><span class="token interpolation-punctuation punctuation">${</span>first<span class="token punctuation">[</span><span class="token number">0</span><span class="token punctuation">]</span><span class="token interpolation-punctuation punctuation">}</span></span><span class="token string"> </span><span class="token interpolation"><span class="token interpolation-punctuation punctuation">${</span>first<span class="token punctuation">[</span><span class="token number">1</span><span class="token punctuation">]</span><span class="token interpolation-punctuation punctuation">}</span></span><span class="token template-punctuation string">`</span></span><br><br>  <span class="token comment">// iterate through points adding L (line) commands to path</span><br>  points<span class="token punctuation">.</span><span class="token function">forEach</span><span class="token punctuation">(</span><span class="token parameter">val</span> <span class="token operator">=></span> <span class="token punctuation">{</span><br>    path <span class="token operator">+=</span> <span class="token template-string"><span class="token template-punctuation string">`</span><span class="token string"> L </span><span class="token interpolation"><span class="token interpolation-punctuation punctuation">${</span>val<span class="token punctuation">[</span><span class="token number">0</span><span class="token punctuation">]</span><span class="token interpolation-punctuation punctuation">}</span></span><span class="token string"> </span><span class="token interpolation"><span class="token interpolation-punctuation punctuation">${</span>val<span class="token punctuation">[</span><span class="token number">1</span><span class="token punctuation">]</span><span class="token interpolation-punctuation punctuation">}</span></span><span class="token template-punctuation string">`</span></span><br>  <span class="token punctuation">}</span><span class="token punctuation">)</span><br><br>  <span class="token comment">// close path down from the last point to bottom-right, bottom-left, then back to start</span><br>  path <span class="token operator">+=</span> <span class="token template-string"><span class="token template-punctuation string">`</span><span class="token string"> L </span><span class="token interpolation"><span class="token interpolation-punctuation punctuation">${</span>width<span class="token interpolation-punctuation punctuation">}</span></span><span class="token string"> </span><span class="token interpolation"><span class="token interpolation-punctuation punctuation">${</span>height<span class="token interpolation-punctuation punctuation">}</span></span><span class="token string"> L 0 </span><span class="token interpolation"><span class="token interpolation-punctuation punctuation">${</span>height<span class="token interpolation-punctuation punctuation">}</span></span><span class="token string"> Z</span><span class="token template-punctuation string">`</span></span><br><br>  <span class="token keyword">return</span> path<br><span class="token punctuation">}</span></code></pre>
<h2>Attempt 1 — Random</h2>
<p>In my keenness, I jumped straight in with my first thoughts. I use <code>Math.random</code> to work out where the next position is and keep going until I've done the whole width:</p>
<iframe class="codepen" height="380" style="width:100%;" scrolling="no" title="CodePen Embed" src="https://codepen.io/anon/embed/qBpvNYN?height=380&theme-id=dark&default-tab=result" frameborder="0" loading="lazy" allowtransparency="true" allowfullscreen="true"><p><a href="https://codepen.io/accudio/pen/qBpvNYN" target="_blank" rel="noopener">See the Pen</a></p></iframe>
<p>Ah. Not quite what I was going for, less like a mountain ridge and more like a bed of nails. Maybe the issue is that I'm used fixed intervals, so I tried random intervals too:</p>
<iframe class="codepen" height="380" style="width:100%;" scrolling="no" title="CodePen Embed" src="https://codepen.io/anon/embed/vYpPKaJ?height=380&theme-id=dark&default-tab=result" frameborder="0" loading="lazy" allowtransparency="true" allowfullscreen="true"><p><a href="https://codepen.io/accudio/pen/vYpPKaJ" target="_blank" rel="noopener">See the Pen</a></p></iframe>
<p>Yeah, that looks really cool! Not what I'm wanting though - it has too much randomness and most of the time it just doesn't make sense.</p>
<h2>Attempt 2 — Midpoint Displacement</h2>
<p>After my first attempt, I actually did some research on terrain generation. I wanted something very simple I could implement myself in JavaScript and very fast.</p>
<p>I discovered the Midpoint Displacement Algorithm which seems to fit the bill perfectly. It's a simple algorithm and isn't very often used in modern games thanks to a lack of sudden steep inclines, overhangs and such, but for a mostly rolling ridge as I wanted it's perfect.</p>
<p>A short summary is how it works is by drawing out a straight line, and then splitting it into two segments at the midpoint. We then take that midpoint and 'displace' it—move it upwards or downwards— by a random amount. We then take the two segments and do the same thing, splitting them in two on a midpoint and displacing that midpoint. Each iteration, we reduce the amount each midpoint can move so as the segments get smaller we get finer and finer detail.</p>
<p>If you're interested in the theory behind it or the implementation I would recommend reading <a href="https://bitesofcode.wordpress.com/2016/12/23/landscape-generation-using-midpoint-displacement/">&quot;Landscape generation using midpoint displacement&quot; by Bites of Code</a>. This is a great article about implementing this in Python, and it explains whats happening and why really well. I found it when I was implementing it myself, and most of my code is a JS adaptation of their Python code.</p>
<p>I made a few tweaks and voila! Check out the demo for the code and result:</p>
<iframe class="codepen" height="380" style="width:100%;" scrolling="no" title="CodePen Embed" src="https://codepen.io/anon/embed/VwyRjaj?height=380&theme-id=dark&default-tab=result" frameborder="0" loading="lazy" allowtransparency="true" allowfullscreen="true"><p><a href="https://codepen.io/accudio/pen/VwyRjaj" target="_blank" rel="noopener">See the Pen</a></p></iframe>
<p>This works really well, and generates extremely quickly. You can play with the variables at the top of the file to change the dimensions, fiedlity and roughness.</p>
<p>By running the output SVG through SVGO it ends up being pretty small too! This is exactly the method you see around my site at the time of writing.</p>
<h2>Attempt 3 — Noise?</h2>
<p>I did make a third attempt, using Simplex noise to generate a terrain map with higher fidelity, cliffs, overhangs and flatter regions. I didn't get very far with it however, as I didn't particularly like the effect for the divider—it pulled away too much attention. It was also significantly slower to generate the SVG was quite a lot larger so I ended up ditching it and sticking with attempt 2.</p>
<p>It is very fun to play with terrain generation though so I'd love to play with this some more in future!</p>
<h2>Conclusion</h2>
<p>Here's the final demo of the divider, as used on my site:</p>
<iframe class="codepen" height="380" style="width:100%;" scrolling="no" title="CodePen Embed" src="https://codepen.io/anon/embed/VwyRjaj?height=380&theme-id=dark&default-tab=result" frameborder="0" loading="lazy" allowtransparency="true" allowfullscreen="true"><p><a href="https://codepen.io/accudio/pen/VwyRjaj" target="_blank" rel="noopener">See the Pen</a></p></iframe>
<p>I implemented this server-side with <a href="https://github.com/Accudio/alistair-shepherd/blob/main/src/_includes/utils/shortcodes/divider.js">an Eleventy Shortcode</a>, but as it's JavaScript you could easily use it on the client instead. That's what I've done in the demos throughout this post.</p>
<p>There are so many examples of where web designers and developers can learn from game design and development. Video games have so many examples of unique, creative and interesting challenges and solutions in their design and development that we could learn from. This is definitely a case where a fairly standard technique used by game developers can be used for creative result on the web.</p>
<p>Now go have a play and implement something like this yourself! Look at any games you play, or find out a little bit about an industry you aren't as familiar with and see if there's anything you can learn from to make more creative and cool websites!</p>

      <hr>
      <p>If you have any comments or feedback on this article, let me know! I'd love to hear your thoughts, go ahead and <a href="mailto:alistair@accudio.com">send me an email</a> at alistair@accudio.com or <a href="https://front-end.social/@accudio" rel="noopener noreferrer">contact me on Mastodon</a>.</p>
      <p>The post <a href="https://www.alistairshepherd.uk/writing/svg-generative-ridges/">SVG generative mountain ridge dividers</a> appeared first on <a href="https://alistairshepherd.uk">alistairshepherd.uk</a>.</p>
    ]]>
      </content>
    </entry>
  
    
    <entry>
      <title>Async Alpine — Asynchronous Alpine component loading</title>
      <link href="https://www.alistairshepherd.uk/writing/async-alpine/"/>
      <updated>2022-05-30T00:00:00Z</updated>
      <id>https://www.alistairshepherd.uk/writing/async-alpine/</id>
      <content type="html">
        <![CDATA[
      <p>I've released a new side-project, a library called <a href="https://github.com/Accudio/async-alpine">Async Alpine</a>! Async Alpine is a wrapper for the fantastic <a href="https://alpinejs.dev/">Alpine.js</a> JavaScript library, giving you greater control of component loading and adding asynchronous/lazy loading to Alpine. This makes loading components when visible, for certain screen widths, on events and more, super simple!</p>
<p>If you want to dive straight in check out the <a href="https://github.com/Accudio/async-alpine">Async Alpine GitHub repo</a> for the docs!</p>
<p>This is a companion post to <a href="/writing/code-splitting-alpine">&quot;Code Splitting in Alpine.js&quot;</a>, a post that goes into more depth on why we would want to split up our code more, and the principle on how we do it in Alpine. Check out that for more depth, stay here for how to get started with Async Alpine.</p>
<h2>Getting Started</h2>
<p>There's a few different methods of installing, depending on how you load Alpine.</p>
<p>If you load Alpine via a CDN script, do the same with Async Alpine:</p>
<pre class="language-html"><code class="language-html"><span class="token tag"><span class="token tag"><span class="token punctuation">&lt;</span>script</span> <span class="token attr-name">src</span><span class="token attr-value"><span class="token punctuation attr-equals">=</span><span class="token punctuation">"</span>https://unpkg.com/async-alpine/dist/async-alpine.script.js<span class="token punctuation">"</span></span><span class="token punctuation">></span></span><span class="token script"></span><span class="token tag"><span class="token tag"><span class="token punctuation">&lt;/</span>script</span><span class="token punctuation">></span></span><br><span class="token tag"><span class="token tag"><span class="token punctuation">&lt;</span>script</span> <span class="token attr-name">src</span><span class="token attr-value"><span class="token punctuation attr-equals">=</span><span class="token punctuation">"</span>https://unpkg.com/alpinejs/dist/cdn.min.js<span class="token punctuation">"</span></span><span class="token punctuation">></span></span><span class="token script"></span><span class="token tag"><span class="token tag"><span class="token punctuation">&lt;/</span>script</span><span class="token punctuation">></span></span></code></pre>
<p>For npm installations, install with <code>npm install async-alpine</code> and include it in your bundle:</p>
<pre class="language-js"><code class="language-js"><span class="token keyword">import</span> Alpine <span class="token keyword">from</span> <span class="token string">'Alpine.js'</span><span class="token punctuation">;</span><br><span class="token keyword">import</span> AsyncAlpine <span class="token keyword">from</span> <span class="token string">'async-alpine'</span><br><span class="token function">AsyncAlpine</span><span class="token punctuation">(</span>Alpine<span class="token punctuation">)</span><br><span class="token comment">// any components or plugins go here</span><br>Alpine<span class="token punctuation">.</span><span class="token function">start</span><span class="token punctuation">(</span><span class="token punctuation">)</span></code></pre>
<h2>Components</h2>
<p>Async Alpine leans into and relies on ES Modules to dynamically import components. This supports all modern browsers and keeps the package fast and lightweight.</p>
<p>An ES Module Alpine component looks like this:</p>
<pre class="language-js"><code class="language-js"><span class="token keyword">export</span> <span class="token keyword">default</span> <span class="token keyword">function</span> <span class="token function">myComponent</span><span class="token punctuation">(</span><span class="token punctuation">)</span> <span class="token punctuation">{</span><br>  <span class="token keyword">return</span> <span class="token punctuation">{</span><br>    <span class="token literal-property property">message</span><span class="token operator">:</span> <span class="token string">'hello!'</span><span class="token punctuation">,</span><br><br>    <span class="token function">init</span><span class="token punctuation">(</span><span class="token punctuation">)</span> <span class="token punctuation">{</span><br>      <span class="token function">alert</span><span class="token punctuation">(</span><span class="token keyword">this</span><span class="token punctuation">.</span>message<span class="token punctuation">)</span><br>    <span class="token punctuation">}</span><br>  <span class="token punctuation">}</span><br><span class="token punctuation">}</span></code></pre>
<p>It's common to write Alpine components like this already, so this might look familiar! The key thing is that the file uses <code>export default</code> ES Module syntax to export the component function.</p>
<p>If you ship handwritten JavaScript then you can write your component similar to above, pop it in your assets directory and you're golden!</p>
<p>If you process JS with a build tool or bundler you may need to do some work to output modules in the right format. This will depend on your build tool, but the majority are easy to set up:</p>
<ul>
<li><a href="https://webpack.js.org/configuration/output/#module-definition-systems">WebPack — output library type 'module'`</a></li>
<li><a href="https://rollupjs.org/guide/en/#configuration-files">Rollup — format 'es'</a></li>
<li><a href="https://esbuild.github.io/api/#format-esm">ESBuild — format 'esm'</a></li>
<li><a href="https://parceljs.org/features/targets/#outputformat">Parcel &amp;mdash: outputFormat 'esmodule'</a></li>
</ul>
<h2>Importing Components</h2>
<p>You write your Alpine components as normal with Async Alpine, and add a couple of attributes to your component root:</p>
<pre class="language-html"><code class="language-html"><span class="token tag"><span class="token tag"><span class="token punctuation">&lt;</span>div</span><br>	<span class="token attr-name">x-data</span><span class="token attr-value"><span class="token punctuation attr-equals">=</span><span class="token punctuation">"</span>myComponent<span class="token punctuation">"</span></span><br>	<span class="token attr-name">ax-load</span><br>	<span class="token attr-name">ax-load-src</span><span class="token attr-value"><span class="token punctuation attr-equals">=</span><span class="token punctuation">"</span>/assets/my-component.js<span class="token punctuation">"</span></span><br><span class="token punctuation">></span></span><span class="token tag"><span class="token tag"><span class="token punctuation">&lt;/</span>div</span><span class="token punctuation">></span></span></code></pre>
<p>The <code>ax-load</code> attribute declares that this is managed by Async Alpine and declares the strategy—we'll leave that as the default for now.</p>
<p>In <code>ax-load-src</code> you add the public URL of your component module. Here you can use relative URLs (<code>/assets/component.js</code>) or remote full URLs including the domain name (<code>https://example.com/component.js</code>).</p>
<p>Now your component is loaded asynchronously when it is present on the page! No need to load it everywhere in your bundle, it'll only load if it's needed.</p>
<h2>Loading Strategies</h2>
<p>We left <code>ax-load</code> as the default previously, but we can be more specific than that! By default Async Alpine will load components 'eagerly', that means if the component is used on the page it will be downloaded as soon as it's found.</p>
<p>That's perfect for high-priority components at the top of the page, but for less important components we can use other rules to load them when they're needed. This component will load when it comes into view using the 'visible' strategy:</p>
<pre class="language-html"><code class="language-html"><span class="token tag"><span class="token tag"><span class="token punctuation">&lt;</span>div</span><br>	<span class="token attr-name">x-data</span><span class="token attr-value"><span class="token punctuation attr-equals">=</span><span class="token punctuation">"</span>myComponent<span class="token punctuation">"</span></span><br>	<span class="token attr-name">ax-load</span><span class="token attr-value"><span class="token punctuation attr-equals">=</span><span class="token punctuation">"</span>visible<span class="token punctuation">"</span></span><br>	<span class="token attr-name">ax-load-src</span><span class="token attr-value"><span class="token punctuation attr-equals">=</span><span class="token punctuation">"</span>/assets/my-component.js<span class="token punctuation">"</span></span><br><span class="token punctuation">></span></span><span class="token tag"><span class="token tag"><span class="token punctuation">&lt;/</span>div</span><span class="token punctuation">></span></span></code></pre>
<p>At the time of writing, Async Alpine has six different strategies:</p>
<ul>
<li><code>eager</code>—load the component as soon as it's found;</li>
<li><code>idle</code>—waits until the browser isn't busy;</li>
<li><code>visible</code>—load the component when the user scrolls close to it;</li>
<li><code>media</code>—in response to any browser media query!</li>
<li><code>event</code>—use a DOM event to trigger loading at your command;</li>
<li><code>parent</code>—for loading nested components smarter.</li>
</ul>
<p>These can be used as you'd like, and even combined for really advanced loading strategies!</p>
<p>A carousel at the bottom of a page that only displays on small screens? <code>ax-load=&quot;visible | media (max-width: 768px)&quot;</code> has you sorted!</p>
<p>An 3D model viewer that runs on a button press but needs to ensure it's parent is loaded first? <code>ax-load=&quot;event | parent&quot;</code> will do the trick.</p>
<h2>Project Status</h2>
<p>Async Alpine is still in development and the API isn't totally stable quite yet. That said, I use it successfully on production sites and would encourage you to give it a try!</p>
<p>There's more information, installation instructions, examples, and advanced settings in the <a href="https://github.com/Accudio/async-alpine">documentation on GitHub</a>.</p>
<p>If you use it or have any questions I'd love to chat with you! I am very keen to see how it can help improve the websites you build, get feedback from people who have used it and make improvements in response!</p>

      <hr>
      <p>If you have any comments or feedback on this article, let me know! I'd love to hear your thoughts, go ahead and <a href="mailto:alistair@accudio.com">send me an email</a> at alistair@accudio.com or <a href="https://front-end.social/@accudio" rel="noopener noreferrer">contact me on Mastodon</a>.</p>
      <p>The post <a href="https://www.alistairshepherd.uk/writing/async-alpine/">Async Alpine — Asynchronous Alpine component loading</a> appeared first on <a href="https://alistairshepherd.uk">alistairshepherd.uk</a>.</p>
    ]]>
      </content>
    </entry>
  
    
    <entry>
      <title>Code Splitting in Alpine.js</title>
      <link href="https://www.alistairshepherd.uk/writing/code-splitting-alpine/"/>
      <updated>2022-05-30T00:00:00Z</updated>
      <id>https://www.alistairshepherd.uk/writing/code-splitting-alpine/</id>
      <content type="html">
        <![CDATA[
      <p>I've recently published a library called Async Alpine that adds asynchronous or lazy loading of components to Alpine.js! It can help you write faster, more efficient sites using Alpine.js. Check out my <a href="/writing/async-alpine">Async Alpine blog post</a> for more info!</p>
<h2>How I use Alpine.js</h2>
<p>At <a href="https://serieseight.com">Series Eight</a> we love the JavaScript framework <a href="https://alpinejs.dev">Alpine.js</a>. Alpine is a lightweight JS library that allows you to add interactivity and JS to static HTML with attributes using vanilla browser DOM. It makes it easy to add JavaScript to your existing HTML rather than replacing it like more traditional JS, but puts click events and text insertion right where it happens in the markup like more modern.</p>
<p>This post isn't meant as an introduction to Alpine, for that check out <a href="https://css-tricks.com/alpine-js-the-javascript-framework-thats-used-like-jquery-written-like-vue-and-inspired-by-tailwindcss/">Alpine.js: The JavaScript Framework That’s Used Like jQuery, Written Like Vue, and Inspired by TailwindCSS</a> on <a href="https://css-tricks.com/">CSS Tricks</a>.</p>
<p>There are two main ways of writing components in Alpine. For simple components you can write a JavaScript object as a string in the <code>x-data</code> attribute of your component. This is great if there's not a huge amount going on, but you can't import other libraries or transpile modern syntax, and it lacks the syntax and formatting help that writing in <code>.js</code> files provides.<br>
For these more complex components, you can write them as a JS function and declare them with <code>Alpine.data()</code>.</p>
<p>I tend to write basic components inline but use JS components for anything with more than a couple variables/methods.</p>
<h2>Code loading in JavaScript</h2>
<p>Moving away from Alpine for a moment, one area that the entire JS and web industry has been grappling and struggling with is code splitting. When you use libraries or frameworks, the easiest implementation is often bundling everything into a single JavaScript file that has the framework and all your components.</p>
<p>This is simple to build, but in terms of performance can be inefficient and makes little sense. Components appear in different places throughout the site and a user is unlikely to encounter them all. For example, in a typical eCommerce store you might have a static landing page. On the same site the product pages load a 3D model library to show off the products. In a bundling pattern, even though the landing page is static he user has to download all JavaScript required for the entire site—including the 3D library for the product page—even if it isn't needed. This will delay how quickly the page will become interactive on the first load, for a page and features that the user doesn't need yetmdash;and might never need if they don't visit that part of the site.</p>
<p>An alternative to bundling might be to split your application up into several chunks, pages or components. Tools like <a href="https://nextjs.org">Next.js</a> will do this for you automatically to mitigate the cost of JavaScript in large sites, but the developer doesn't get much control over this.</p>
<p>Recent tools like <a href="https://astro.build">Astro</a> and <a href="https://slinkity.dev">Slinkity</a> take this a step further, allowing the developer to specify when a component should load. In many cases few users will ever scroll down to see a component at the bottom of a long page. We may want to load a component like that only when a user scrolls down far enough to be likely to view it.</p>
<p>Custom implementations have existed for a while, but these tools are the first I've seen to bring it to modern component-based JS libraries.</p>
<h2>Alpine component loading</h2>
<p>Back to Alpine, when they declare components with <code>Alpine.data()</code> people often bundle them into a single JS file. Alpine needs to register components before it runs, meaning the easiest solution is a single bundle and we definitely don't have the fine control like with Astro/Slinkity. This is back to our issue with loading code that might never be used.</p>
<p>I found that as I worked on Alpine sites the bundle got larger and larger, often with components that were rarely visited or used. One of my sites was 120kB extra for a fancy animation on the bottom of a landing page, that was seen by 0.2% of visitors. This is the easy path with JavaScript frameworks and Alpine, loading that component when it was needed would be tricky and might mean abandoning Alpine.</p>
<h2>Code Splitting in Alpine.js</h2>
<p>One way we can handle loading components asynchronously in Alpine is by taking control of when Alpine runs components. When it starts Alpine scans through the entire DOM and finds elements with the <code>x-data</code> attribute and runs them. If we rename <code>x-data</code> to something else in our code, then Alpine won't see and run it.</p>
<p>We'd need to consider the other Alpine attributes like <code>x-show</code>, <code>:class</code> and <code>@click</code> and a handful of other Alpine functionalities.</p>
<p>Once we've renamed those attributes and Alpine has started, we can control when we add the components again. We load the component when we'd like to based on certain conditions, use <code>Alpine.data()</code> to prepare it and then rename the attributes back again. Alpine will pick up on the change, see the component and run it as normal.</p>
<p>With that we have a lightweight way to load components on-demand!</p>
<h2>Async Alpine</h2>
<p>That's a minimal setup that does the basics, but we could go a lot further to add different loading strategies and to support the standard Alpine syntax. I've done that work and released it as a library called <a href="https://github.com/Accudio/async-alpine">Async Alpine</a>!</p>
<p>I've written another post focusing on it more—<a href="/writing/async-alpine">Async Alpine — Asynchronous Alpine component loading</a>—and you can find <a href="https://github.com/Accudio/async-alpine">more info about Async Alpine on GitHub</a></p>
<p>It came out of wanting Astro/Slinkity style loading for Alpine components, and I've been working on it the past couple of months. With more control over component loading I could build faster, more efficient websites without changing the syntax of a library I was familiar with. It has advanced loading options including immediately, on idle, when visible, using a media query, DOM events or <strong>any</strong> combination of those.</p>
<p>It's still in development and will need more testing before I'd consider it stable, but I've used it on several production websites with great success. If you're familiar with Alpine I'd encourage you to give it a try and see how it works for you!</p>

      <hr>
      <p>If you have any comments or feedback on this article, let me know! I'd love to hear your thoughts, go ahead and <a href="mailto:alistair@accudio.com">send me an email</a> at alistair@accudio.com or <a href="https://front-end.social/@accudio" rel="noopener noreferrer">contact me on Mastodon</a>.</p>
      <p>The post <a href="https://www.alistairshepherd.uk/writing/code-splitting-alpine/">Code Splitting in Alpine.js</a> appeared first on <a href="https://alistairshepherd.uk">alistairshepherd.uk</a>.</p>
    ]]>
      </content>
    </entry>
  
    
    <entry>
      <title>My comments to Competition and Markets Authority on mobile browser competition</title>
      <link href="https://www.alistairshepherd.uk/writing/cma-mobile-browsers-mir/"/>
      <updated>2022-07-18T00:00:00Z</updated>
      <id>https://www.alistairshepherd.uk/writing/cma-mobile-browsers-mir/</id>
      <content type="html">
        <![CDATA[
      <p>This is my personal submission to the <a href="https://www.gov.uk/cma-cases/mobile-browsers-and-cloud-gaming">UK's Competition and Markets Authority regarding mobile browser competition</a>. Feel free to take inspiration/borrow from it and submit your own response by 22nd July 2022. Check out <a href="https://twitter.com/OpenWebAdvocacy/status/1537676458478702594">@OpenWebAdvocacy</a> for more info!</p>
<hr>
<p>In response to Competition and Markets Authority’s Mobile browsers and cloud gaming MIR consultation</p>
<p>I am a Front End Web Developer resident in the UK, working for the London-based web agency Series Eight. This response is my personal concerns and comments rather than the position of any of my clients or employers, past or present. Series Eight is a website design and development agency that builds award-winning eCommerce and marketing websites for companies and brands within the UK. As a web developer at Series Eight I work with browsers and websites extensively and my comments come from my experience developing a large number of websites and web apps.</p>
<p>I have previously submitted my experiences and thoughts—specifically in relation to the monopoly of the Webkit browser engine on Apple's iOS—as a response to the Competition and Markets Authority’s Mobile ecosystems market study. <a href="https://assets.publishing.service.gov.uk/media/622773fbd3bf7f1581a6eace/Developer_-_Alistair_Shepherd.pdf">My response is available as a PDF on gov.uk</a></p>
<p>In my previous response I specifically focus on the difficulties I and other developers face due to the lack of browser competition on iOS. Safari is a competant browser but in my experience has a large number of bugs and quirks that are difficult to deal with, and cost me and companies I have worked for a significant amount of time on every project to diagnose and fix. This cost is passed on to the companies and clients I work with, and due to a lack of any competition within the browser space on iOS Apple has little motivation to address these issues. In fact, due to the money Apple makes from native app development, their motivation may be in making web development more challenging on iOS to stifle a competitor platform.</p>
<p>As raised in the CMA's Mobile ecosystems market study, Google also engages in and exploits their entrenched mobile browser monopoly on Android. By utilising their position as the dominant browser, search engine, mobile operating system and email provider, Google is able to ignore the interests of consumers in mobile browser in favour of their own interests. This comes in the form of Google products requiring or 'suggesting' the use of Google Chrome to increase their browser market share, and features and functionalities added to Chrome to support their business interests elsewhere including in Search and Advertising, at the cost of consumer interests like privacy.<br>
Despite Android supporting browser engine diversity and choice on paper, other browsers do not face the same preferential treatment as Google Chrome. Many Google apps will ignore the default browser and instead use Chrome, some APIs and functionalities are only available in Chrome, and features like Progressive Web Apps and Trusted Web Apps in some cases require Google Chrome.<br>
I have spent hours diagnosing issues with websites to find the user reporting them was not aware that Google Search had ignored their default browser and their normal settings were not available, and development of PWAs and TWAs are more challenging when cross-browser development is not possible.</p>
<p>The inability to compete with native apps using Progressive Web Apps fully—particularly on iOS—also has a big impact on my work and the businesses I have worked with. Progressive Web Apps are extremely accessible for development, allowing for the creation of a simple app in a fraction of the time and complexity of a native app. This is fantastic for allowing smaller agencies and businesses to innovate on the web and on mobile devices and to reach consumers. However the poor support for PWA features by Safari and by not allowing them in the App Store, Apple forces app development to be difficult, time consuming and extremely expensive. I have spoken with many companies who would have liked an app to compete with their larger competitors but are unable to afford the huge costs in developing a native app.</p>
<p>I consider the analysis of the features of concern and the reference tests in regards to mobile browsers is correct and accurately reflects my experiences and concerns. Mobile browsers are a key part of participation in modern society and the market being fair and competitive for consumers is vital.</p>
<p>I believe that the opening of mobile browsers will be an almost universal benefit. As we have seen in the industry of web browsers in the past during the mid-2000's for Internet Explorer, a monopoly without reasonable competitor browsers causes progress and standards in web development to fall and negatively impacts developers and consumers significantly. By opening this up consumers can make meaningful choices to respect their preferences towards certain features, companies and privacy. Developers will be able to rely on more competitive browser engines prioritising bugs, security and interopability. In this manner I believe it will also benefit Safari and Chrome, motivating their teams to push for further improvement and innovation.</p>
<p>I broadly agree with the remedies presenting by the CMA in the Mobile browsers and cloud gaming MIR consultation in regards to mobile browsers. I think that removing Apple's restrictions on browser engine diversity and mandating equal functionality for browsers is extremely important to restore competition in the mobile browser ecosystem.</p>
<p>In regards to the suggestion for requiring choice screens, I don't believe that they would be an effective or required remedy. I am not sure they are particularly effective at preventing users from choosing the default browser, and it simply moves the goalposts on what browsers are allowed to 'compete'. When Microsoft implemented a browser choice screen in Windows I saw it pose more confusion to users than help. I am of the belief that a market where all other things are equal besides pre-installation solves the issue without the need for choice screens. I believe that the prevelance of Chrome on MacOS and Windows devices show that good browsers can easily overcome pre-installation.</p>
<p>An additional remedy I think is important to improving competition of mobile devices is in regards to ensuring PWAs are treated equally to native apps. I believe that Apple particularly should be required to allow PWA submission to their App Store so PWAs can compete effectively with native apps. In my experience this would significantly open up access to app development to a huge number of developers and businesses that could not afford native app development or the management of multiple platforms.</p>
<p>It is also extremely important that browser choice is always respected, especially when alternative browsers are available. Google, Apple, and third-parties including Meta are known to ignore default browser choice in some circumstances. A requirement for diversity is not effective when apps can ignore a user's preference, so I think it is important that platforms and apps be required to respect the default browser as a potential additional remedy.</p>
<p>I believe the remedies as proposed and I have mentioned would be sufficient and effective at allowing for competition in mobile browsers and in addressing the entrenched market power of Apple and Google in the mobile browser ecosystem. I have less experience with the cloud gaming industry, however as a consumer the concerns and proposed remedies do seem accurate and appropriate to ensure competition in this area.</p>
<p>My comments are of a personal capacity and do not represent any organisation I work for. I would like my response to be attributed to me by name, and you have my permission to publish or quote from this document with or without attribution.</p>
<p>Best regards,<br>
Alistair Shepherd</p>

      <hr>
      <p>If you have any comments or feedback on this article, let me know! I'd love to hear your thoughts, go ahead and <a href="mailto:alistair@accudio.com">send me an email</a> at alistair@accudio.com or <a href="https://front-end.social/@accudio" rel="noopener noreferrer">contact me on Mastodon</a>.</p>
      <p>The post <a href="https://www.alistairshepherd.uk/writing/cma-mobile-browsers-mir/">My comments to Competition and Markets Authority on mobile browser competition</a> appeared first on <a href="https://alistairshepherd.uk">alistairshepherd.uk</a>.</p>
    ]]>
      </content>
    </entry>
  
    
    <entry>
      <title>My 2022 round-up</title>
      <link href="https://www.alistairshepherd.uk/writing/2022-roundup/"/>
      <updated>2022-12-30T00:00:00Z</updated>
      <id>https://www.alistairshepherd.uk/writing/2022-roundup/</id>
      <content type="html">
        <![CDATA[
      <p>Happy Hogmanay, New Year, or January depending on what you celebrate!</p>
<p>This is a wrap-up of my year, which has been extremely busy for me. It's mostly for myself to look back on, but if you're interested then great!</p>
<p>I didn't used to 'get' round-up/wrap-up posts, but over the past year I've got into journalling and taking note of what I've been doing so I can look back on it. A post like this suddenly makes sense now I realise it's primarily for me to look back on—not to show off!</p>
<h2>State of the Browser Conference</h2>
<p>The big thing for me was speaking at the State of the Browser conference in London. There I gave a talk called &quot;Creative web: Building dynamic websites for work and play&quot;. It was my first experience speaking at a conference, and it was absolutely fantastic. The organisers, other speakers and attendees were so welcoming, friendly and helpful and I owe a massive thank you to all of them.</p>
<p>The recording will be available soon, but <a href="https://accud.io/sotb">my slides are public</a> now if you're interested! I'll also be giving an improved version again in future so watch out for that!</p>
<p>A particular shout-out to Bruce Lawson for his kind and touching advice on where to aim if I needed to projectile vomit. ❤️</p>
<h2>My work at Series Eight</h2>
<p>It has been a great year for working at <a href="https://serieseight.com">Series Eight</a>, I've been involved in some awesome projects, interesting technical challenges, and some really exciting changes in the company. We've got a fantastic team that I am loving working with, and I finally got to <a href="https://serieseight.com/journal/ericeira-2022-our-biggest-and-best-team-trip-yet">meet them for our team trip in Portugal back in September</a> (the disadvantage of working remotely)!</p>
<p>One thing I'm particularly thrilled with about our work over 2022 is how we're really taking the accessibility of our sites seriously. It's a slow process updating old sites and putting a greater priority on it in our processes, but change is coming and I'm so happy to be working somewhere that cares.</p>
<p>At the end of this year I was also promoted to Lead Developer of the team, starting when we return in January! It'll be a new challenge doing a bit more management and admin rather than just coding, but I'm looking forward to working to support the team.</p>
<h2>My work - development, speaking and blogs</h2>
<p>I've done a bit more open-source and side projects this year than previous, particularly working on my project <a href="https://asyncalpine.dev">Async Alpine</a>. Check that out if you haven't already, it's a library for Alpine.js that supercharges component loading and I am so proud of it. Thanks to the SeriesEight team and GitHub contributors for suggestions and feedback!</p>
<p>This year I also built <a href="https://github.com/accudio/cead-consent">Cead Consent</a> and <a href="https://github.com/accudio/sailwind">Sailwind</a>. Cead consent is a GDPR/cookie/tracking consent manager that handles enabling/disabling tracking scripts and pixels for your users. Sailwind is a fluid spacing utility generator for TailwindCSS, I'm pretty excited about the potential it offers for translating designs. Both are open-source and I've got some plans for them both coming up!</p>
<p>Recently with my growing distaste for Twitter I've started helping with and contributing towards <a href="https://github.com/tweetback/tweetback/">Tweetback</a>, a tool to host your tweet archive. I've made a handful fo contributions so far but I'm very keen to continue to help out and make it easier for people to own their content.</p>
<p>In speaking, other than at State of the Browser I've spoken about Image CDNs on quite a few occasions. I did several meet-ups earlier in the year and even a Twitch stream! A big thanks to everyone who hosted events I've spoken at over 2022.</p>
<p>Finally, I've written some blog posts I'm really happy with! Here's the list in reverse order:</p>
<ul>
<li><a href="https://alistairshepherd.uk/writing/cma-mobile-browsers-mir/">My comments to Competition and Markets Authority on mobile browser competition</a></li>
<li><a href="https://alistairshepherd.uk/writing/code-splitting-alpine/">Code Splitting in Alpine.js</a></li>
<li><a href="https://alistairshepherd.uk/writing/async-alpine/">Async Alpine — Asynchronous Alpine component loading</a></li>
<li><a href="https://alistairshepherd.uk/writing/svg-generative-ridges/">SVG generative mountain ridge dividers</a></li>
<li><a href="https://alistairshepherd.uk/writing/cead-consent/">Managing tracking consent with Cead Consent</a></li>
</ul>
<h2>Personal life</h2>
<p>Like many people—and the year in general—2022 was a bit of a rollercoaster for me. There's been some rough spots but this post is focusing on the positives!</p>
<p>Although Covid is of course still ongoing, with some care I've managed to meet up with friends I mostly hadn't seen since pre-pandemic which has been the highlight of my year. We went on a holiday to Dubrovnik, Croatia together and when this post is published I'm hosting people for a new years party.</p>
<p>Earlier this year I made myself a goal to climb every hill in the Pentlands over 400m, a range of hills near Edinburgh. There's 52 in total and I've managed to get through 24 this year. Those are the easy ones though, getting many of the remaining ones will end up being a bit tricky!</p>
<p>Despite not going abroad I've managed to get out skiing a fair bit this year! I went up to Glenshee several times at the end of last season and managed a day before christmas this season. So excited to get up there more this year!</p>
<p>This is the obligatory note that I've moved from Twitter to <a href="https://mastodon.scot/@accudio">Mastodon</a> which has been a breath of fresh air. I'm really enjoying the 'Fedi-verse', if you haven't switched already <a href="https://joinmastodon.com">come join us</a>!</p>
<p>Also I'm dying my hair blue now, why did no one tell me it was this easy before?!</p>
<h2>Music, Games, Books and Podcasts</h2>
<p>I've watched, played, listened to and read so much great stuff this year I want to share!</p>
<h3>Music</h3>
<p>It's been a brilliant year for music for me! These are releases I've loved that came out in 2022:</p>
<ul>
<li><a href="https://album.link/d/290951312">Night Flight: Crazy (Work On Me)</a> — I utterly adore Night Flight, they're probably my favourite band ever and the two songs on this release are brilliant. I can not be more excited for their next album!</li>
<li><a href="https://album.link/d/284826852">Yumi Zouma: Present Tense</a> — Fantastic chill indie pop, seeing them live in King Tut's in Glasgow was one of the best gigs I've ever been to;</li>
<li><a href="https://album.link/d/290951312">Gang of Youths: angel in realtime</a> — this one took me by surprised, becoming one of my favourite albums this year in a matter of days;</li>
<li><a href="https://album.link/gb/i/1589853108">Barrie: Barbara</a> — I love chill indie pop, and Barrie nails it perfectly;</li>
<li><a href="https://album.link/d/252701682">White Lies: As I try not to fall apart</a>;</li>
<li><a href="https://album.link/d/295947242">Pale Waves: Unwanted</a>;</li>
<li><a href="https://album.link/d/353205767">Everything Everything: Raw Data Feel</a>;</li>
<li><a href="https://album.link/d/328175587">Conan Grey: Superache</a>.</li>
</ul>
<h3>Games</h3>
<p>I played a bunch of short indie games this year that were astounding. I'll save the commentary for only the top few but I'd love to speak about any with you if you'd like! Not all of them were released in 2022 but I played them all for the first time this year:</p>
<ul>
<li><a href="https://strangehorticulture.com">Strange Horticulture</a> — I'm a sucker for a story-heavy occult game, add mystery elements and set it in the lake district? One of my favourite games ever made;</li>
<li><a href="https://store.steampowered.com/app/1677770/The_Case_of_the_Golden_Idol/">The Case of the Golden Idol</a> — whodunnit mystery that's genuinely challenging with beautiful pixel graphics;</li>
<li><a href="https://www.stanleyparable.com">The Stanley Parable Ultra Deluxe</a> — I loved the first game and this adds a similar experience but totally new. Perfect;</li>
<li><a href="https://www.supergiantgames.com/games/hades/">Hades</a>;</li>
<li><a href="https://silverliningstudio.co/Behindtheframe/">Behind the Frame</a>;</li>
<li><a href="http://iamdeadgame.com">I am Dead</a>;</li>
<li><a href="https://theartfulescape.com">The Artful Escape</a>;</li>
<li><a href="https://www.genesisnoirgame.com">Genesis Noir</a>;</li>
<li><a href="https://www.toppluva.com/GrandMountainAdventure/">Grand Mountain Adventure</a>;</li>
<li><a href="https://store.steampowered.com/app/1049410/Superliminal/">Superliminal</a>;</li>
<li><a href="http://edithfinch.com">What remains of Edith Finch</a>;</li>
<li><a href="http://gorogoa.com">Gorogoa</a>;</li>
<li><a href="https://www.valheimgame.com">Valheim</a>;</li>
<li><a href="http://ethancartergame.com">The Vanishing of Ethan Carter</a>.</li>
</ul>
<h3>Books</h3>
<p>I mostly consume books in the form of audiobooks, but I'll put them in one list with physical books! The top couple are work-related, the rest my normal reading. Again, not just books that came out this year but just ones I read this year.</p>
<ul>
<li><a href="https://www.engmanagement.dev">Engineering Management for the rest of us: Sarah Drasner</a>;</li>
<li><a href="https://www.smashingmagazine.com/printed-books/understanding-privacy/">Understanding Privacy: Heather Burns</a>;</li>
<li><a href="https://www.goodreads.com/book/show/37542581-the-spy-and-the-traitor">The Spy and the Traitor: Ben MacIntyre</a>;</li>
<li><a href="https://www.goodreads.com/book/show/35959740-circe">Circe: Madeline Miller</a>;</li>
<li><a href="https://www.goodreads.com/book/show/13623848-the-song-of-achilles">The song of Achilles: Madeline Miller</a>;</li>
<li><a href="https://www.goodreads.com/book/show/58623865-storyland">Storyland, A New Mythology of Britain: Amy Jeffs</a>;</li>
<li><a href="https://www.goodreads.com/book/show/103749.Persian_Fire">Persian Fire: Tom Holland</a>;</li>
<li><a href="https://www.goodreads.com/book/show/42972023-the-anarchy">The Anarchy: William Dalrymple</a>;</li>
<li><a href="https://www.goodreads.com/book/show/43885149-dominion">Dominion: Tom Holland</a>.</li>
</ul>
<h3>Podcasts</h3>
<p>Only a couple new ones so here's all of the podcasts I've been listening to, new and old:</p>
<ul>
<li><a href="https://shoptalkshow.com">Shop Talk</a>;</li>
<li><a href="https://www.nosuchthingasafish.com">No Such Thing as a Fish</a>;</li>
<li><a href="https://linktr.ee/restishistory">The Rest is History</a>;</li>
<li><a href="https://songexploder.net">Song Exploder</a>.</li>
</ul>
<h2>Photos</h2>
<ul class="auto-grid" role="list"><li>
      <a class="d-b radius" href="/images/sotb.jpeg?width=2000">
        <img
          class="radius"
          src="/images/sotb.jpeg?width=350,height=350"
          srcset="/images/sotb.jpeg?width=400,height=400 400w, /images/sotb.jpeg?width=600,height=600 600w, /images/sotb.jpeg?width=800,height=800 800w, /images/sotb.jpeg?width=1077,height=1077 1077w, /images/sotb.jpeg?width=1500,height=1500 1500w, /images/sotb.jpeg?width=2154,height=2154 2154w"
          sizes="(min-width: 70rem) 21.5rem, (min-width: 54rem) calc(33vw - 2rem), (min-width: 36.8rem) calc(50vw - 2.3rem), calc(100vw - 2.66rem)"
          alt="Me standing and speaking at a podium at State of the Browser"
          width="350"
          height="350"
          loading="lazy"
        >
      </a>
    </li><li>
      <a class="d-b radius" href="/images/s8-team-trip.jpg?width=2000">
        <img
          class="radius"
          src="/images/s8-team-trip.jpg?width=350,height=350"
          srcset="/images/s8-team-trip.jpg?width=400,height=400 400w, /images/s8-team-trip.jpg?width=600,height=600 600w, /images/s8-team-trip.jpg?width=800,height=800 800w, /images/s8-team-trip.jpg?width=1077,height=1077 1077w, /images/s8-team-trip.jpg?width=1500,height=1500 1500w, /images/s8-team-trip.jpg?width=2154,height=2154 2154w"
          sizes="(min-width: 70rem) 21.5rem, (min-width: 54rem) calc(33vw - 2rem), (min-width: 36.8rem) calc(50vw - 2.3rem), calc(100vw - 2.66rem)"
          alt="Series Eight team photo in portugal"
          width="350"
          height="350"
          loading="lazy"
        >
      </a>
    </li><li>
      <a class="d-b radius" href="/images/glenshee-2022.jpg?width=2000">
        <img
          class="radius"
          src="/images/glenshee-2022.jpg?width=350,height=350"
          srcset="/images/glenshee-2022.jpg?width=400,height=400 400w, /images/glenshee-2022.jpg?width=600,height=600 600w, /images/glenshee-2022.jpg?width=800,height=800 800w, /images/glenshee-2022.jpg?width=1077,height=1077 1077w, /images/glenshee-2022.jpg?width=1500,height=1500 1500w, /images/glenshee-2022.jpg?width=2154,height=2154 2154w"
          sizes="(min-width: 70rem) 21.5rem, (min-width: 54rem) calc(33vw - 2rem), (min-width: 36.8rem) calc(50vw - 2.3rem), calc(100vw - 2.66rem)"
          alt="Me grinning at the camera in ski gear with Glenshee ski centre in background"
          width="350"
          height="350"
          loading="lazy"
        >
      </a>
    </li><li>
      <a class="d-b radius" href="/images/graduation.jpg?width=2000">
        <img
          class="radius"
          src="/images/graduation.jpg?width=350,height=350"
          srcset="/images/graduation.jpg?width=400,height=400 400w, /images/graduation.jpg?width=600,height=600 600w, /images/graduation.jpg?width=800,height=800 800w, /images/graduation.jpg?width=1077,height=1077 1077w, /images/graduation.jpg?width=1500,height=1500 1500w, /images/graduation.jpg?width=2154,height=2154 2154w"
          sizes="(min-width: 70rem) 21.5rem, (min-width: 54rem) calc(33vw - 2rem), (min-width: 36.8rem) calc(50vw - 2.3rem), calc(100vw - 2.66rem)"
          alt="Me posing outside in full highland dress with blue-purple kilt"
          width="350"
          height="350"
          loading="lazy"
        >
      </a>
    </li><li>
      <a class="d-b radius" href="/images/dubrovnik.jpeg?width=2000">
        <img
          class="radius"
          src="/images/dubrovnik.jpeg?width=350,height=350"
          srcset="/images/dubrovnik.jpeg?width=400,height=400 400w, /images/dubrovnik.jpeg?width=600,height=600 600w, /images/dubrovnik.jpeg?width=800,height=800 800w, /images/dubrovnik.jpeg?width=1077,height=1077 1077w, /images/dubrovnik.jpeg?width=1500,height=1500 1500w, /images/dubrovnik.jpeg?width=2154,height=2154 2154w"
          sizes="(min-width: 70rem) 21.5rem, (min-width: 54rem) calc(33vw - 2rem), (min-width: 36.8rem) calc(50vw - 2.3rem), calc(100vw - 2.66rem)"
          alt="Me wearing flowery shirt on the Dubrovnik walls"
          width="350"
          height="350"
          loading="lazy"
        >
      </a>
    </li><li>
      <a class="d-b radius" href="/images/pentlands.jpg?width=2000">
        <img
          class="radius"
          src="/images/pentlands.jpg?width=350,height=350"
          srcset="/images/pentlands.jpg?width=400,height=400 400w, /images/pentlands.jpg?width=600,height=600 600w, /images/pentlands.jpg?width=800,height=800 800w, /images/pentlands.jpg?width=1077,height=1077 1077w, /images/pentlands.jpg?width=1500,height=1500 1500w, /images/pentlands.jpg?width=2154,height=2154 2154w"
          sizes="(min-width: 70rem) 21.5rem, (min-width: 54rem) calc(33vw - 2rem), (min-width: 36.8rem) calc(50vw - 2.3rem), calc(100vw - 2.66rem)"
          alt="Selfie at the summit of a Pentland hill"
          width="350"
          height="350"
          loading="lazy"
        >
      </a>
    </li></ul>

      <hr>
      <p>If you have any comments or feedback on this article, let me know! I'd love to hear your thoughts, go ahead and <a href="mailto:alistair@accudio.com">send me an email</a> at alistair@accudio.com or <a href="https://front-end.social/@accudio" rel="noopener noreferrer">contact me on Mastodon</a>.</p>
      <p>The post <a href="https://www.alistairshepherd.uk/writing/2022-roundup/">My 2022 round-up</a> appeared first on <a href="https://alistairshepherd.uk">alistairshepherd.uk</a>.</p>
    ]]>
      </content>
    </entry>
  
    
    <entry>
      <title>GrapheneOS as my daily-driver mobile OS</title>
      <link href="https://www.alistairshepherd.uk/writing/grapheneos/"/>
      <updated>2023-01-15T00:00:00Z</updated>
      <id>https://www.alistairshepherd.uk/writing/grapheneos/</id>
      <content type="html">
        <![CDATA[
      <p>In November 2022 I dropped my previous phone. Actually 'drop' maybe isn't accurate, it was more of a drop-kick into a stone floor. It was pretty beat-up already honestly, this Xiaomi Mi 9T had been dropped countless times and the back was covered in sellotape to keep the glass in. My <em>beautiful</em> drop-kick was a bit too much though, and the screen died completely.</p>
<p>I had already been considering getting a new phone for a bit however, as my trusty 9T certainly had it's problems. I'll get onto some of the software issues later, but also the pop-up selfie camera only popped up about 30% of the time (thanks to a Snowboarding accident). I regularly got strange looks as my phone whirred unhealthily and I shook it upside-down to try to get the camera out.</p>
<p>So at this point I was looking for a new smartphone, and for anyone who isn't a big fan of either Apple or Google as I am you'll be familiar with the dilemma. Android is a privacy and security hell-hole where you're expected to get a new phone almost every half an hour. iOS is possible <em>slightly</em> better for privacy but expects you to sell at least 3 organs to get the cash to buy a phone. Also <a href="/writing/cma-mobile-browsers-mir/">fuck the browser ban</a>.</p>
<p>Thanks to <a href="https://mastodon.neilzone.co.uk/@neil">Neil Brown</a>, I found <a href="https://grapheneos.org">GrapheneOS</a>, a mobile OS which promises to solve all my problems. I've since been using it for a couple months on a Google Pixel 6 and it's been fantastic. This post is about my experience with it to share the love!</p>
<h2>What is GrapheneOS?</h2>
<p>So <a href="https://grapheneos.org">GrapheneOS</a> is Android... kinda. It's basically the open-source version of Android but with loads of added security and privacy functionality. It's open-source, officially supports all the most recent Google Pixel devices and can be fairly easily installed to replace the default Pixel Android.</p>
<p>By default it doesn't include Google apps or services for security and privacy reasons, so Google doesn't have constant access to your device for their nefarious purposes. It does however support android apps, and you can install Google services in a limited way to ensure maximum compatibility while ensuring it doesn't have full control/access.</p>
<p>It sounded fantastic, Android app compatibility including for apps that need Google services, whilst a priority on security and privacy. That's exactly what I want from my phone.</p>
<h2>My previous mobile OS</h2>
<p>So on my previous couple of phones I tried to do something like what GrapheneOS promises. I had a de-googled version of Android, with the <a href="https://microg.org">microg</a> project adding support for apps that needed Google services.</p>
<p>Unfortunately in practice it didn't work great. Props to all devs involved in making it happen, but so many apps didn't work on it. Some intentionally: &quot;Your phone is insecure, fuck off&quot;, some just crashing.</p>
<p>I had to carry a second phone with me that had 'normal' Android on it for my banking apps, most takeaway/food delivery apps, on holidays I was relying on others to order an Uber for me, and mobile gaming was pretty much completely out. There were often workarounds and alternatives for some of these but it was regularly a huge effort just to install an app.</p>
<h2>GrapheneOS installation and set-up</h2>
<p>Graphene has a very fancy web-based UI for installing itself onto devices and extremely good instructions and documentation. It was probably the easiest OS install/flash I've ever dealt with, mobile or otherwise.</p>
<p>Compatibility with different computers seems a bit iffy, I couldn't get it working on my Windows 11 desktop (driver issues probably) and work Macbook (USB C-C cable not being right maybe?) but it worked fine on my personal Windows 10 laptop. If you have issues try different computers you have access to and different cables.</p>
<p>You basically just go through all the steps, doing what you're told and clicking the buttons when prompted. I did have some cases where it seemed to stop at random points so I had to re-do some steps when they didn't finish but eventually they all worked and I got it installed.</p>
<p>Set-up was pretty standard Android, minus all of Google's shitty questions about tracking and such. Overall very easy to get installed and set up.</p>
<h2>Multiple Profiles</h2>
<p>GrapheneOS has full separate user profiles and encourages users to utilise these to isolate different apps from each other to increase privacy and security. On Android every app can see what other apps you have installed on that user and potentially interact with them, so if you split your apps across different users it limits how much each app knows and can potentially affect.</p>
<p>It also allows you to more easily control what apps are running when. If a user is not 'logged in', none of the apps in it can run in the background.</p>
<p>I got really confused about how to set this up at first. I understood the concept but didn't really get the details about what was being suggested? Looking at what other people did confused me further as it varied so much. Some people would have an 'Instagram' user, others would not use the default user at all, I didn't really get what extent I should be using user accounts.</p>
<p>Once I played with it a bit it started to make more sense. I'd suggest thinking about them as different 'contexts'. I ended up with this user structure:</p>
<ul>
<li>Main profile — my 'normal'. Has stuff I use often and anything I want running in the background;</li>
<li>Social Media — social media apps, allowing me to only engage with them when I explicitly want to;</li>
<li>Google Services — this is where I have Google Play services installed for apps that need it. Food delivery apps, games and misc apps I need that can't run in my main profile;</li>
<li>Work — I like being able to have easy-access to my work email and Slack, if I'm out and running late for example. This allows me to keep it out of mind unless necessary;</li>
<li>Private — stuff that I want behind an extra later of security and a different PIN code. Includes banking apps and anything I wouldn't want my parents to be able to switch to!</li>
</ul>
<h2>My thoughts on Graphene after a few months</h2>
<p>I really like GrapheneOS. Quite often software intended for people who are security/privacy minded compromises a lot on usability but the user experience of Graphene is fantastic. It has a handful of issues—more on that in a sec—but none are major enough to override all of the problems it solves with the mobile OS market.</p>
<p>Some of the things I love about it:</p>
<ul>
<li>The user profiles are fantastic for app control;</li>
<li>Flawless compatibility with <em>almost</em> every android app I've tried;</li>
<li>Improved security;</li>
<li>Nice additional android settings and controls;</li>
<li>OS updates that happen in the background that have never had an issue or changed anything unexpectedly (more than I can say about Google and Apple);</li>
<li>Far better privacy—I noticed how Google ads knew considerably less about me and my life after switching;</li>
<li>The camera works as well as stock;</li>
<li>Automatic fully-encrypted app backups;</li>
<li>Gesture-based navigation works brilliantly.</li>
</ul>
<p>In terms of battery life and performance, it's been pretty much exactly as normal Android when I tried it before installing Graphene. Performance seems the exact same, and the battery life might even be a bit better with less tracking and more control over background apps.</p>
<h2>Issues with Graphene</h2>
<p>There's not many issues, but as I said it's not perfect.</p>
<p>The most notable for me is that Facebook Messenger calls often don't come through to me. Even if I have the app open, someone will call me and nothing even comes up until the 'didn't answer' message appears. Messages work and I can call people fine, but until I come up with a solution I have to occasionally check my missed calls. It's not a terrible arrangement, my friends and family know to phone me if it's urgent.</p>
<p>I've also found a handful of apps that don't work, with Google Play Services or without. So far there's been three, all random games from the Play Store that crash on startup. None I'm that fussed about yet so I haven't done any debugging. It is a very small number compared to the total number of apps that work great.</p>
<p>I normally have an always-on VPN and have occasionally had issues with connecting to the internet with it on. This might be my Wireguard VPN client but I didn't have any problems on my last phone. Toggling it off and on tends to sort it out.</p>
<h2>Google Pixel 6</h2>
<p>This post is mostly about GrapheneOS, as the software is what I really care about. If you're planning on buying a phone for Graphene though, I'll mention my experience with the Pixel 6.</p>
<p>I bought a refurbished device rather than new for climate reasons and to not give money to Google directly. After the first device being a store model stolen from an o2 store and unusable, the second one was in perfect nick and as new.</p>
<p>It's a bit big, I think I maybe should have gone for the Pixel 6a as that's slightly smaller, but I manage it okay with fairly big hands. It is slippy so I'd suggest getting a skin or case for it. I've got a <a href="https://www.spigen.com/products/pixel-6-case-liquid-air">Spigen Liquid Air</a> which isn't too thick and offers a bit of protection whilst still feeling pretty premium. I don't like the shape of the back with the big raised camera, but the case makes that a bit less major.</p>
<p>The camera quality is good, I'm not sure it's up to all the hype from the many adverts I've seen but it's better than my previous phones.</p>
<p>The fingerprint sensor is pretty crummy unfortunately, the worst I've used before. It works, but fairly often I have to try multiple times to get in. I originally wondered if this was Graphene, but from some searching it seems to be the phone hardware.</p>
<p>Overall, it's a decent phone for a decent price. A good one to go for if you're buying a new phone for Graphene. I wouldn't recommend it without Graphene though, it's not worth the Google spyware.</p>
<h2>Conclusion</h2>
<p>I'm a big fan of GrapheneOS and it's pretty much nailed the perfect mobile OS for me at the moment. I've got more control over my phone, how I use it, how apps run on it, and who it reports back to than I ever have before.</p>
<p>I would highly recommend it for anyone considering a new phone, especially if you're considering privacy, security or control over your device.</p>
<p>Feel free to <a href="mastodon.scot/@accudio">message me</a> or <a href="mailto:alistair@accudio.com">email me</a> if you have any questions about it! I'd be happy to help. 👋</p>

      <hr>
      <p>If you have any comments or feedback on this article, let me know! I'd love to hear your thoughts, go ahead and <a href="mailto:alistair@accudio.com">send me an email</a> at alistair@accudio.com or <a href="https://front-end.social/@accudio" rel="noopener noreferrer">contact me on Mastodon</a>.</p>
      <p>The post <a href="https://www.alistairshepherd.uk/writing/grapheneos/">GrapheneOS as my daily-driver mobile OS</a> appeared first on <a href="https://alistairshepherd.uk">alistairshepherd.uk</a>.</p>
    ]]>
      </content>
    </entry>
  
    
    <entry>
      <title>Displaying Hillwalking routes on the web with GPX files and Leaflet</title>
      <link href="https://www.alistairshepherd.uk/writing/hillwalking-maps-with-leaflet/"/>
      <updated>2023-01-23T00:00:00Z</updated>
      <id>https://www.alistairshepherd.uk/writing/hillwalking-maps-with-leaflet/</id>
      <content type="html">
        <![CDATA[
      <p>I do a lot of hillwalking and use <a href="https://osmand.net">OsmAnd</a> for maps on my phone and route tracking. I have a couple paper maps and a compass just in case, but for most walks my phone does great. OsmAnd uses Open Street maps for it's data which is fantastic, and has a set of map tiles that look pretty similar to Ordnance Survey maps and I like.</p>
<p>I track my routes for posterity so I know how long particular walks have taken for if I come back to them again some time. I don't really do much else with them though, and with an itch to build a new site for myself I wondered if I could do something with the route files.</p>
<h2>Enter Leaflet</h2>
<p><a href="https://leafletjs.com">Leaflet</a> is a fantastic open source JavaScript library for displaying and interacting with Open Street Maps on the web. Think of it like the Google Maps JS API but totally free, lacking the horrendous tracking, and more customisable.</p>
<p>Leaflet is pretty easy to get started with, I'm going to use the CDN url:</p>
<iframe class="codepen" height="380" style="width:100%;" scrolling="no" title="CodePen Embed" src="https://codepen.io/anon/embed/yLqvROg?height=380&theme-id=dark&default-tab=result" frameborder="0" loading="lazy" allowtransparency="true" allowfullscreen="true"><p><a href="https://codepen.io/accudio/pen/yLqvROg" target="_blank" rel="noopener">See the Pen</a></p></iframe>
<p>In the HTML I'm loading the script and CSS for Leaflet and have an HTML element for the rendered map. In the CSS I'm just displaying that map fullscreen.</p>
<p>In the JavaScript we initialise a map on the <code>#map</code> element and set the starting co-ordinates and zoom level. We then add a Tilelayer, which is basically the images that make up the map, along with credit details and then add it to the map.</p>
<p>There we go! We've got a simple map.</p>
<h2>GPX Route Files</h2>
<p>I can export my routes from OsmAnd in the format GPX. GPX is short for &quot;GPS Exchange&quot;, and it's an open standard that's used by lots of different GPS devices and programs for sharing data about routes. It's pretty standard and many GPS and tracking apps will be able to export to it.</p>
<p>I've exported a GPX file to try, and the first thing I notice is it's over 600kB! I'm pretty particular about web performance but I think it's fair to say that is far too large if I can do anything about it.</p>
<p>Thankfully there are various tools to help reduce that filesize. My route tracker is set up to log every 5 seconds so I guess over a 4 hour walk that is a lot of data points, but in reality I don't need nearly that many to show the rough route online. I found <a href="https://gpx.studio">gpx studio</a> to be handy for editing GPX files, it allows you to import and export, has some handy tools and importantly allows you to view the route on a map. I used the &quot;Reduce number of tracking points&quot; option (the two diagonal arrows) to reduce the number of tracking points from 2,300 to 390 which reduced the file size a lot. I chose that number by cranking it down until I started to lose some of the fidelity I wanted in the route line.</p>
<p>After reducing the points we got down to 36kB. A lot better!</p>
<h2>Displaying the route on the map</h2>
<p>The next step was getting the route onto my map. It turns out there's a plugin called <a href="https://github.com/mpetazzoni/leaflet-gpx">leaflet-gpx</a> that makes that really easy. I upload my GPX file somewhere, include the plugin JS in a script tag and can then use <code>new L.GPX</code> to create the route:</p>
<iframe class="codepen" height="380" style="width:100%;" scrolling="no" title="CodePen Embed" src="https://codepen.io/anon/embed/YzjeBMK?height=380&theme-id=dark&default-tab=result" frameborder="0" loading="lazy" allowtransparency="true" allowfullscreen="true"><p><a href="https://codepen.io/accudio/pen/YzjeBMK" target="_blank" rel="noopener">See the Pen</a></p></iframe>
<p>I've also got an event listener after this to fit the map to the route when it's loaded. This means I can remove the <code>.setView()</code> when creating the map and not worry about the latitude, longitude or zoom level, Leaflet will handle it for me.</p>
<p>When loading the plugin over a CDN I found the included icons didn't load by default, and I had to add the below code to make them work:</p>
<pre class="language-js"><code class="language-js"><span class="token keyword">new</span> <span class="token class-name">L<span class="token punctuation">.</span>GPX</span><span class="token punctuation">(</span><span class="token string">'...'</span><span class="token punctuation">,</span> <span class="token punctuation">{</span><br>  <span class="token literal-property property">async</span><span class="token operator">:</span> <span class="token boolean">true</span><span class="token punctuation">,</span><br>  <span class="token literal-property property">marker_options</span><span class="token operator">:</span> <span class="token punctuation">{</span><br>    <span class="token literal-property property">startIconUrl</span><span class="token operator">:</span> <span class="token string">'https://cdn.jsdelivr.net/npm/leaflet-gpx@1.7.0/pin-icon-start.png'</span><span class="token punctuation">,</span><br>    <span class="token literal-property property">endIconUrl</span><span class="token operator">:</span> <span class="token string">'https://cdn.jsdelivr.net/npm/leaflet-gpx@1.7.0/pin-icon-end.png'</span><span class="token punctuation">,</span><br>    <span class="token literal-property property">shadowUrl</span><span class="token operator">:</span> <span class="token string">'https://cdn.jsdelivr.net/npm/leaflet-gpx@1.7.0/pin-shadow.png'</span><br>  <span class="token punctuation">}</span><br><span class="token punctuation">}</span><span class="token punctuation">)</span></code></pre>
<p>This is fantastic and is almost exactly what I'm looking for! How the map looks isn't ideal though, the default Open Street Map tiles aren't very relevant for hillwalking, prioritising roads and the golf course in this example. Ideally I'd have it highlight the different terrain, walking paths, fences, and feature contour lines at the very least!</p>
<h2>Tilelayers</h2>
<p>Earlier we added a Tilelayer to Leaflet, which I said was basically the images that made up the map. In that example we used the tiles provided by OpenStreetMap, the, 'official' and recommended default of Leaflet. That's just one option however, and anyone can use the data from OpenStreetMap to make their own map tiles looking however they'd like.</p>
<p>Turns out there's a bunch of different Tilelayers out there for all sorts of different purposes, and Leaflet supports lots of them! There's a mixture of open-source, free, and licensed tile providers, with <a href="https://wiki.openstreetmap.org/wiki/Raster_tile_providers">many listed in the Open Street Map Wiki</a>. By finding a tile provider that is more focused on hiking and outdoor pursuits I could get a map that was a lot more suitable for my purpose.</p>
<p>I managed to find <a href="https://www.thunderforest.com">Thunderforest</a>, which is a company that offers a range of maps, and crucially an <a href="https://www.thunderforest.com/maps/outdoors/">&quot;Outdoors&quot; map</a> that has contour lines, forests, walking trails and hillshading—perfect for my hillwalking maps! They run their maps as a commercial product with API keys, but have a generous free &quot;Hobby Project&quot; tier that is perfect for my use.</p>
<p>We can switch it out really easily by changing the URL used in the <code>L.tileLayer</code> function. Thunderforest also requires us to update the attribution in the bottom-right corner of that map so I've done that here too.</p>
<pre class="language-js"><code class="language-js"><span class="token keyword">new</span> <span class="token class-name">L<span class="token punctuation">.</span>tileLayer</span><span class="token punctuation">(</span><br>  <span class="token string">'https://tile.thunderforest.com/outdoors/{z}/{x}/{y}.png?apikey=xxx'</span><span class="token punctuation">,</span><br>  <span class="token punctuation">{</span> <span class="token literal-property property">attribution</span><span class="token operator">:</span> <span class="token string">'Maps © &lt;a href="https://www.thunderforest.com">Thunderforest&lt;/a>, Data © &lt;a href="https://www.openstreetmap.org/copyright">OpenStreetMap contributors&lt;/a>'</span> <span class="token punctuation">}</span><br><span class="token punctuation">)</span><span class="token punctuation">.</span><span class="token function">addTo</span><span class="token punctuation">(</span>map<span class="token punctuation">)</span><span class="token punctuation">;</span></code></pre>
<h2>Result</h2>
<p>Put that all together and we have a handy little widget that allows me to view and display my walking routes on the web!</p>
<iframe class="codepen" height="380" style="width:100%;" scrolling="no" title="CodePen Embed" src="https://codepen.io/anon/embed/KKBQJOy?height=380&theme-id=dark&default-tab=result" frameborder="0" loading="lazy" allowtransparency="true" allowfullscreen="true"><p><a href="https://codepen.io/accudio/pen/KKBQJOy" target="_blank" rel="noopener">See the Pen</a></p></iframe>
<p>This was a fun little experiment that was to test the waters for a potential future project. If you're interested to see what I do with this (no prizes for guessing) then <a href="https://mastodon.scot/@accudio">follow me on Mastodon</a>!</p>

      <hr>
      <p>If you have any comments or feedback on this article, let me know! I'd love to hear your thoughts, go ahead and <a href="mailto:alistair@accudio.com">send me an email</a> at alistair@accudio.com or <a href="https://front-end.social/@accudio" rel="noopener noreferrer">contact me on Mastodon</a>.</p>
      <p>The post <a href="https://www.alistairshepherd.uk/writing/hillwalking-maps-with-leaflet/">Displaying Hillwalking routes on the web with GPX files and Leaflet</a> appeared first on <a href="https://alistairshepherd.uk">alistairshepherd.uk</a>.</p>
    ]]>
      </content>
    </entry>
  
    
    <entry>
      <title>Searching for a Mastodon app for Android</title>
      <link href="https://www.alistairshepherd.uk/writing/mastodon-android/"/>
      <updated>2023-03-26T00:00:00Z</updated>
      <id>https://www.alistairshepherd.uk/writing/mastodon-android/</id>
      <content type="html">
        <![CDATA[
      <p>I've been using the Fediverse via Mastodon since October 2022, as a pretty much complete replacement for Twitter. I'm loving it! The community feels a lot more friendly and accessible than Twitter has done in a long time and I'm very happy to be out of the Twitter shitshow.</p>
<p>You can find me at <a href="https://mastodon.scot/@accudio">accudio@mastodon.scot</a> and if you're not on the Fediverse yet come join us!</p>
<p>Now onto the actual topic of this post, trying out various Mastodon or Fediverse apps for Android! I've been using <a href="https://twidere.com">Twidere</a> since I started, originally because it supported both Twitter and Mastodon and therefore allowed me to transition slowly. I removed Twitter after a few weeks but kept the app as it's decent. Unfortunately however lately I've had it crashing upon seeing particular posts. That means I have to clear the cache and data before re-using it — not ideal!</p>
<p>Hence my search for a new app, but first...</p>
<h2>How I use social media and 'must-haves'</h2>
<p>I used Twitter — and use Mastodon — in a very particular way. Normally I check in maybe 2-3 times a day, depending on how busy I am. When I start my app, I want it to be exactly where I left off when I last used it. I then go through 'catching up' on what's been posted since I last checked, and once I'm done I finish and get back to work/play. When I next check it I continue from where I finished.</p>
<p>I have a pretty curated feed so this doesn't tend to take me very long and means I don't miss anything from the folks I really care about. I use mutes, hide retweets and separate RSS feeds to keep track of others in a less instant form.</p>
<p>This way of using Twitter and Mastodon is ideal for me, and is the most healthy relationship I've had with Social Media as once I'm 'caught up' there's no more scrolling to do!</p>
<p>I have just one 'must-have' for a Mastodon app (beyond the very basics of loading posts) and from how I use it that'll probably be pretty clear! I need the app to keep track where I am in my feed, no exceptions. I don't mind if loading more posts is automatic or manual, but it has to happen around my scroll position so I don't lose track of where I am. Any other features are a bonus!</p>
<h2>The contendors</h2>
<p>First, thank you so much to <a href="https://mastodon.scot/@accudio/110057755928969826">everyone who gave me recommendations when I asked</a> last week, most of these suggestions came from those recommendations.</p>
<p>If there are any good ones you know of that I haven't mentioned then please let me know!</p>
<h3>Tusky</h3>
<p><a href="https://tusky.app">Tusky</a> was the most common recommendation I got and it's pretty good! Modern design, very quick, great support for Mastodon features like Polls, editing, displaying of alt text, all of which is beyond Twidere.</p>
<p>Unfortunately, when I'm scrolling up and tap &quot;Load More&quot; it pins me to the top of the new posts rather than the bottom, requiring me to scroll back down to find where I was beforehand. I tried to stick with it for a bit because I really like the other features but it just frustrated me unfortunately.</p>
<h3>Husky</h3>
<p><a href="https://husky.adol.pw">Husky</a> is a fork of Tusky and has pretty similar features, mainly focused around Pleroma servers. I don't use a Pleroma server so most of these aren't too major for me as a Mastodon user.</p>
<p>Sadly it faces the same issue as Tusky with loading new posts</p>
<h3>Official Mastodon app</h3>
<p>I like the design and feel of the <a href="https://f-droid.org/packages/org.joinmastodon.android/">official Mastodon app</a> but compared to Tusky and Husky the comparitive lack of customisation kinda sucks. That said, if it can load posts in the way I want it to then that's nothing...</p>
<p>Once again, no it can't. Tapping &quot;Load more posts&quot; pins you to the top (newest) of the loaded posts rather than the bottom (oldest). Another fail :(</p>
<h3>Megalodon and Moshidon</h3>
<p><a href="https://sk22.github.io/megalodon/">Megalodon</a> is a fork of the official Mastodon app that adds support for some pretty handy features like a Federated timeline, more control over posting and image descriptiong viewing. <a href="https://lucasggamerm.github.io/moshidon/">Moshidon</a> is a fork of Megalodon which also adds support for remote local timelines.</p>
<p>Both are cool but suffer from the same issue as the official app when it comes to loading posts.</p>
<h3>Mastodon website</h3>
<p>I don't necessarily need an app really, I'm perfectly happy to use a website or Progressive Web App instead if that works better. That said, thanks to Gogle being anti-competitive, monopolistic jerks my phone doesn't support Progressive Web Apps properly. So I'm restricted to the Mastodon website of my server as viewed through my browser, no PWA install superpowers.</p>
<p>And this ends up worse than the apps unfortunately! If I load it up having not used it in a few hours the page loads fresh and I'm placed at the top of my feed with the latest post. Absolutely no can do unfortunately.</p>
<p>I can't test how it works as a proper PWA, but giving it the benefit of the doubt I'm blaming this one on Google.</p>
<h3>Elk</h3>
<p><a href="https://elk.zone">Elk</a> is a third-party website and PWA for a few different Fediverse servers that is pretty great for customisation and accessibility.</p>
<p>It's another casualty of Google it seems, loaded at the latest post losing track of where I was. It's a great website though so I might switch my desktop use to it, but it doesn't work for my mobile use.</p>
<h3>FediLab</h3>
<p><a href="https://fedilab.app">FediLab</a> is the best shot yet. It seems a bit rougher than the other apps but works extremely well, with most of the customisation I'd want and loads of super handy features like showing Alt text, automatic privacy-friendly translation, and support for alternative frontends for sites like YouTube, Twitter and Instagram.</p>
<p>This is the first app that copes with my style of browsing the feed! It keeps track of my place in the feed and when I scroll up there's a &quot;Fetch more messages&quot; button that lets me choose whether I want to be placed at the newest first or the oldest — ideal!</p>
<p>I've been using FediLab for about a week, and it's sadly not the success story I hoped it would be. In theory it handles keeping track of scroll position fine but in practice it's not perfect. One in every 3 or 4 opens of the app it will misplace me by a certain amount up or down, making me spend time working out where I was before. Sometimes I think it's just not saving my position during an entire session and the next time I'll be back to where I started.</p>
<p>I really want to stick with it because it's a really solid app and nice experience but after a week of use I don't think I can.</p>
<h3>Twidere</h3>
<p>So that leaves me... exactly where I started? Even with the crashing and all of the Mastodon features it's missing, <a href="https://twidere.com">Twidere</a> meets my requirement of how I use the app the best. It's tracking of scroll position is rock-solid and that part of it hasn't had any issues at ll whilst I've been using it.</p>
<p>In terms of the crashing, it seems to happen about once every 4-5 days on average, and I haven't put the time into working out exactly what's causing it. To fix it, I need to clear the data and cache of the app and restart it. I'm still logged in at that point, but it's lost all of my customisation. However, there's a way to export your settings to a file so I can re-import that after it's crashed to fairly quickly get back to where I was.</p>
<p>It's a faff, and means when it does crash I lose where I was but that's a lot less frequently for any of the other apps.</p>
<p>I'll miss all of the features of the others, when I want to vote in a poll for example I have to open the post in my browser, copy the URL and paste it into my home server. A bloody pain, but I don't use polls that often. I think the thing that I miss the most is not being able to see an image's alt text within the app, making it hard to keep my my policy of &quot;No alt no boost&quot;.</p>
<h2>Finally</h2>
<p>So there we go, that's a summary of how I switched my Mastodon app usage from Twidere to Twidere! If I've missed a setting on any of the apps that would make them work better, or you have any other suggestions then please let me know!</p>
<p>And once again, join me on the Fediverse at <a href="https://mastodon.scot/@accudio">accudio@mastodon.scot</a>, and if you aren't on the Fediverse then come join us! <a href="https://joinmastodon.org">joinmastodon.org</a> is a great place to get started.</p>

      <hr>
      <p>If you have any comments or feedback on this article, let me know! I'd love to hear your thoughts, go ahead and <a href="mailto:alistair@accudio.com">send me an email</a> at alistair@accudio.com or <a href="https://front-end.social/@accudio" rel="noopener noreferrer">contact me on Mastodon</a>.</p>
      <p>The post <a href="https://www.alistairshepherd.uk/writing/mastodon-android/">Searching for a Mastodon app for Android</a> appeared first on <a href="https://alistairshepherd.uk">alistairshepherd.uk</a>.</p>
    ]]>
      </content>
    </entry>
  
    
    <entry>
      <title>Simple, cheap GeoIP API using Vercel Edge functions</title>
      <link href="https://www.alistairshepherd.uk/writing/vercel-geoip/"/>
      <updated>2023-04-30T00:00:00Z</updated>
      <id>https://www.alistairshepherd.uk/writing/vercel-geoip/</id>
      <content type="html">
        <![CDATA[
      <p>Need to look up a users' approximate location based on their IP address? Don't want to opt for a third-party GeoIP service or integrate it into your backend?</p>
<p>Turns out that <a href="https://vercel.com">Vercel</a> makes it super easy to set up a simple GeoIP service for yourself!</p>
<p>If you just want the code you can find the repo at <a href="https://github.com/Accudio/vercel-geoip">github.com/Accudio/vercel-geoip</a> and demo at <a href="https://accudio-geoip.vercel.app/">accudio-geoip.vercel.app</a>. You can fork that repository and deploy it to your own Vercel account to use yourself!</p>
<p>I have also published a very similar post (almost identical to be honest, it's mostly copied) about how to do the <a href="/writing/netlify-geoip">same with Netlify</a>.</p>
<p>Read on for a deeper explanation, and let me know if you have any thoughts or issues!</p>
<h2>Background</h2>
<p>For a couple projects I'm currently working on, recently I had need for a Geolocation API. Nothing too major, just getting a users very rough location based on their IP address, to tailor their default experience of language, currency, or laws.</p>
<p>There are a TON of Geolocation API services with various pricing, trustworthiness and privacy/tracking policies. I looked at a few, but the per-lookup pricing and lack of certainty around trusting a third-party with our users' IP addresses was a bit of a deterrent.</p>
<h2>Vercel and Geolocation Headers</h2>
<p>If you haven't heard of Vercel before, it's a hosting company that specialises in JAMStack sites, similar to Netlify. It's a good platform for static sites, JavaScript-based frameworks and serverless/edge functions.</p>
<p>It's the serverless and edge functions that are the key to this setup. Serverless and edge functions allow us to run a node.js script on each request, responding dynamically. Serverless functions run on centralised servers (they're pretty badly named!), Edge functions are a bit more restrictive and run directly on the CDN nodes allowing for a potentially faster or lighter response.</p>
<p>These functions can be combined with <a href="https://vercel.com/docs/concepts/edge-network/headers#x-vercel-ip-country_">Vercel's HTTP headers with geolocation information</a>. We can send that data back on the request in a JSON format, and then use that within our front-end JavaScript.</p>
<h2>The code</h2>
<p>As most of the examples of Vercel's functions rely on Next.js, it's a bit tricky to find how to set up functions without it. For my own later reference and to avoid you having to go through the same research, I'm going through the full process!</p>
<h3>1. Initialising</h3>
<p>First we need to initialise our repo, npm project and install the Vercel packages.</p>
<pre class="language-sh"><code class="language-sh"><span class="token function">mkdir</span> vercel-geoip <span class="token operator">&amp;&amp;</span> <span class="token builtin class-name">cd</span> vercel-geoip<br><span class="token function">git</span> init<br><span class="token function">npm</span> init <span class="token parameter variable">-y</span><br><span class="token function">npm</span> i <span class="token parameter variable">-D</span> vercel<br><span class="token function">npm</span> i @vercel/edge</code></pre>
<h3>2. Trying out an edge function</h3>
<p>In Vercel projects functions are placed within an <code>api/</code> directory, so let's create an <code>api/index.js</code> file. This would run on any requests to <code>/api/</code>. Within it, we're going to put the very basics of a edge function that has a basic text response:</p>
<pre class="language-js"><code class="language-js"><span class="token comment">// api/index.js</span><br><span class="token keyword">export</span> <span class="token keyword">const</span> config <span class="token operator">=</span> <span class="token punctuation">{</span><br>  <span class="token comment">// Specify this function as an edge function rather than a serverless function</span><br>	<span class="token literal-property property">runtime</span><span class="token operator">:</span> <span class="token string">"edge"</span><br><span class="token punctuation">}</span><span class="token punctuation">;</span><br><br><span class="token comment">// We export the function that runs on each request, which receives the `request`</span><br><span class="token comment">// parameter with data about the current request. We'll use this later</span><br><span class="token keyword">export</span> <span class="token keyword">default</span> <span class="token keyword">function</span> <span class="token punctuation">(</span><span class="token parameter">request</span><span class="token punctuation">)</span> <span class="token punctuation">{</span><br>  <span class="token comment">// respond to the request with the content "hello world!"</span><br>  <span class="token keyword">return</span> <span class="token keyword">new</span> <span class="token class-name">Response</span><span class="token punctuation">(</span><span class="token string">'hello world!'</span><span class="token punctuation">)</span><br><span class="token punctuation">}</span></code></pre>
<p>To test our function, we can run <code>npx vercel dev</code> to run the Vercel development server. This will ask you to link the project to your Vercel account and some details about the project. You can leave those details as default.</p>
<p>Now, if you visit the dev URL in your browser and add <code>/api</code> — <a href="https://localhost:5000/api">probably <code>localhost:5000/api</code></a> you should see &quot;hello world!&quot;.</p>
<h3>3. The Geolocation bit</h3>
<p>Now let's amend our <code>index.js</code> file to include the Geolocation bits:</p>
<pre class="language-js"><code class="language-js"><span class="token comment">// api/index.js</span><br><span class="token comment">// Import the geolocation and ipAddress helpers</span><br><span class="token keyword">import</span> <span class="token punctuation">{</span> geolocation<span class="token punctuation">,</span> ipAddress <span class="token punctuation">}</span> <span class="token keyword">from</span> <span class="token string">"@vercel/edge"</span><span class="token punctuation">;</span><br><br><span class="token keyword">export</span> <span class="token keyword">const</span> config <span class="token operator">=</span> <span class="token punctuation">{</span><br>	<span class="token literal-property property">runtime</span><span class="token operator">:</span> <span class="token string">"edge"</span><span class="token punctuation">,</span><br><span class="token punctuation">}</span><span class="token punctuation">;</span><br><br><span class="token keyword">export</span> <span class="token keyword">default</span> <span class="token keyword">function</span> <span class="token punctuation">(</span><span class="token parameter">request</span><span class="token punctuation">)</span> <span class="token punctuation">{</span><br>	<span class="token comment">// The geolocation helper pulls out the geoIP headers from the</span><br>	<span class="token keyword">const</span> geo <span class="token operator">=</span> <span class="token function">geolocation</span><span class="token punctuation">(</span>request<span class="token punctuation">)</span> <span class="token operator">||</span> <span class="token punctuation">{</span><span class="token punctuation">}</span><span class="token punctuation">;</span><br>	<span class="token comment">// The IP helper does the same function for the user's IP address</span><br>	<span class="token keyword">const</span> ip <span class="token operator">=</span> <span class="token function">ipAddress</span><span class="token punctuation">(</span>request<span class="token punctuation">)</span> <span class="token operator">||</span> <span class="token keyword">null</span><br><br>	<span class="token comment">// Output the Geolocation data and IP address as a JSON object, and</span><br>	<span class="token comment">// set the content type to make it easier to handle when requested</span><br>	<span class="token keyword">return</span> <span class="token keyword">new</span> <span class="token class-name">Response</span><span class="token punctuation">(</span><br>		<span class="token constant">JSON</span><span class="token punctuation">.</span><span class="token function">stringify</span><span class="token punctuation">(</span><span class="token punctuation">{</span><br>			<span class="token operator">...</span>geo<span class="token punctuation">,</span><br>			ip<span class="token punctuation">,</span><br>		<span class="token punctuation">}</span><span class="token punctuation">)</span><span class="token punctuation">,</span><br>		<span class="token punctuation">{</span><br>			<span class="token literal-property property">headers</span><span class="token operator">:</span> <span class="token punctuation">{</span> <span class="token string-property property">"content-type"</span><span class="token operator">:</span> <span class="token string">"application/json"</span> <span class="token punctuation">}</span><span class="token punctuation">,</span><br>		<span class="token punctuation">}</span><br>	<span class="token punctuation">)</span><span class="token punctuation">;</span><br><span class="token punctuation">}</span></code></pre>
<p>Now this won't work in the dev server as Vercel doesn't inject the geolocation headers there, but if you open the function at least it shouldn't error. You can get a preview deployment to test it on the Vercel servers by running <code>npx vercel</code>.</p>
<p>If you visit the <code>/api</code> route on your preview URL you'll get the Geolocation data of your IP address! Neat!</p>
<h3>4. Cross Origin Resource Sharing</h3>
<p>If we try to call this on a different website with JavaScript, we're going to run into CORS issues. CORS — Cross Origin Resource Sharing — is a way browsers prevent websites from using a browser to access content they shouldn't have access to, like resources from a local network. This means as things currently stands, a browser won't let us access the content from our API request with <code>fetch</code>.</p>
<p>To allow us to use the API within JavaScript in a browser, we need to tell the browser to allow CORS. We can do this by adding some HTTP Headers, via a <code>vercel.json</code> config file in root of our project:</p>
<pre class="language-json"><code class="language-json"><span class="token comment">// vercel.json</span><br><span class="token punctuation">{</span><br>  <span class="token property">"headers"</span><span class="token operator">:</span> <span class="token punctuation">[</span><br>    <span class="token punctuation">{</span><br>      <span class="token property">"source"</span><span class="token operator">:</span> <span class="token string">"(.*)"</span><span class="token punctuation">,</span><br>      <span class="token property">"headers"</span><span class="token operator">:</span> <span class="token punctuation">[</span><br>        <span class="token punctuation">{</span> <span class="token property">"key"</span><span class="token operator">:</span> <span class="token string">"Access-Control-Allow-Origin"</span><span class="token punctuation">,</span> <span class="token property">"value"</span><span class="token operator">:</span> <span class="token string">"*"</span> <span class="token punctuation">}</span><span class="token punctuation">,</span><br>        <span class="token punctuation">{</span> <span class="token property">"key"</span><span class="token operator">:</span> <span class="token string">"Access-Control-Allow-Methods"</span><span class="token punctuation">,</span> <span class="token property">"value"</span><span class="token operator">:</span> <span class="token string">"GET,OPTIONS"</span> <span class="token punctuation">}</span><br>      <span class="token punctuation">]</span><br>    <span class="token punctuation">}</span><br>  <span class="token punctuation">]</span><br><span class="token punctuation">}</span></code></pre>
<p>This is taken from Vercel's <a href="https://vercel.com/guides/how-to-enable-cors">&quot;How can I enable CORS on Vercel?&quot; guide</a>. Since this is a relatively straightforward API we don't really need a lot of the parameters in that article, so I've simplified it to allowing all origins, and only the GET and OPTIONS methods.</p>
<p>There is one thing to note with the above code however, the <code>Access-Control-Allow-Origin</code> header allows all origins to make a request to the API. In most cases that might be okay, but you may want to prevent other sites from using your API, especially if you start hitting Vercel's usage limits.</p>
<p>You can <a href="https://developer.mozilla.org/en-US/docs/Web/HTTP/CORS#access-control-allow-origin">whitelist a single origin</a> by adding it to the <code>Access-Control-Allow-Origin</code> header instead of <code>*</code>. You could also include the CORS headers within the edge function depending on the requesting Origin for multiple origins. I haven't run into that problem yet though, so consider that a further exercise for the reader!</p>
<h3>5. Root rewrite (optional)</h3>
<p>The final touch is a rewrite so we can hit our API at the root URL <code>/</code>, instead of having to include <code>api/</code> on every request. With Vercel we can do that with a few more lines to <code>vercel.json</code>:</p>
<pre class="language-json"><code class="language-json"><span class="token comment">// vercel.json</span><br><span class="token punctuation">{</span><br>  <span class="token property">"headers"</span><span class="token operator">:</span> <span class="token punctuation">[</span><span class="token punctuation">]</span><span class="token punctuation">,</span><br>  <span class="token property">"rewrites"</span><span class="token operator">:</span> <span class="token punctuation">[</span><br>    <span class="token punctuation">{</span> <span class="token property">"source"</span><span class="token operator">:</span> <span class="token string">"/"</span><span class="token punctuation">,</span> <span class="token property">"destination"</span><span class="token operator">:</span> <span class="token string">"/api/"</span> <span class="token punctuation">}</span><br>  <span class="token punctuation">]</span><br><span class="token punctuation">}</span></code></pre>
<h3>6. Deploy and test!</h3>
<p>We can deploy the API to Vercel with <code>npx vercel --prod</code>, or link the project via the Vercel website to a Git repo on GitHub, GitLab or similar. Access the API at the Vercel URL, <a href="https://accudio-geoip.vercel.app">for example <code>accudio-geoip.vercel.app</code></a> and there we go!</p>
<p>This is the result I get when visiting that URL (IP obfuscated for privacy):</p>
<pre class="language-json"><code class="language-json"><span class="token punctuation">{</span><br>  <span class="token property">"city"</span><span class="token operator">:</span><span class="token string">"Loughborough"</span><span class="token punctuation">,</span><br>  <span class="token property">"country"</span><span class="token operator">:</span><span class="token string">"GB"</span><span class="token punctuation">,</span><br>  <span class="token property">"countryRegion"</span><span class="token operator">:</span><span class="token string">"ENG"</span><span class="token punctuation">,</span><br>  <span class="token property">"region"</span><span class="token operator">:</span><span class="token string">"lhr1"</span><span class="token punctuation">,</span><br>  <span class="token property">"latitude"</span><span class="token operator">:</span><span class="token string">"52.7681"</span><span class="token punctuation">,</span><br>  <span class="token property">"longitude"</span><span class="token operator">:</span><span class="token string">"-1.2026"</span><span class="token punctuation">,</span><br>  <span class="token property">"ip"</span><span class="token operator">:</span><span class="token string">"XX.XX.XX.X"</span><br><span class="token punctuation">}</span></code></pre>
<p>It's definitely not perfect, to start I'm in Edinburgh, Scotland not Loughborough, England! City and Country Region should maybe be taken with a pinch of salt, but that's something I run into with GeoIP systems all over the web so it's clearly not just Vercel. (interestingly, my <a href="/writing/netlify-geoip">Netlify post</a> had similar but slightly different results)</p>
<p>For the purposes of country though it's accurate, and the City and Region may be helpful to set a default that a user can later change.</p>
<h3>7. Using the API within JavaScript</h3>
<p>We can use this within JavaScript on another website like so, but keep in mind you may need to switch from using <code>await</code> to <code>.then()</code> depending on your setup.</p>
<pre class="language-js"><code class="language-js"><span class="token keyword">const</span> geoRequest <span class="token operator">=</span> <span class="token keyword">await</span> <span class="token function">fetch</span><span class="token punctuation">(</span><span class="token string">'https://accudio-geoip.vercel.app'</span><span class="token punctuation">)</span><br><span class="token keyword">const</span> geo <span class="token operator">=</span> <span class="token keyword">await</span> geoRequest<span class="token punctuation">.</span><span class="token function">json</span><span class="token punctuation">(</span><span class="token punctuation">)</span><br><br>console<span class="token punctuation">.</span><span class="token function">log</span><span class="token punctuation">(</span>geo<span class="token punctuation">.</span>country<span class="token punctuation">)</span><br><span class="token comment">// GB</span></code></pre>

      <hr>
      <p>If you have any comments or feedback on this article, let me know! I'd love to hear your thoughts, go ahead and <a href="mailto:alistair@accudio.com">send me an email</a> at alistair@accudio.com or <a href="https://front-end.social/@accudio" rel="noopener noreferrer">contact me on Mastodon</a>.</p>
      <p>The post <a href="https://www.alistairshepherd.uk/writing/vercel-geoip/">Simple, cheap GeoIP API using Vercel Edge functions</a> appeared first on <a href="https://alistairshepherd.uk">alistairshepherd.uk</a>.</p>
    ]]>
      </content>
    </entry>
  
    
    <entry>
      <title>Simple, cheap GeoIP API using Netlify Edge functions</title>
      <link href="https://www.alistairshepherd.uk/writing/netlify-geoip/"/>
      <updated>2023-05-01T00:00:00Z</updated>
      <id>https://www.alistairshepherd.uk/writing/netlify-geoip/</id>
      <content type="html">
        <![CDATA[
      <p>Need to look up a users' approximate location based on their IP address? Don't want to opt for a third-party GeoIP service or integrate it into your backend?</p>
<p>Turns out that <a href="https://netlify.com">Netlify</a> makes it super easy to set up a simple GeoIP service for yourself!</p>
<p>If you just want the code you can find the repo at <a href="https://github.com/Accudio/netlify-geoip">github.com/Accudio/netlify-geoip</a> and demo at <a href="https://accudio-geoip.netlify.app/">accudio-geoip.netlify.app</a>. You can fork that repository and deploy it to your own Netlify account to use yourself!</p>
<p>I have also published a very similar post (almost identical to be honest, it's mostly copied) about how to do the <a href="/writing/vercel-geoip">same with Vercel</a>.</p>
<p>Read on for a deeper explanation, and let me know if you have any thoughts or issues!</p>
<h2>Background</h2>
<p>For a couple projects I'm currently working on, recently I had need for a Geolocation API. Nothing too major, just getting a users very rough location based on their IP address, to tailor their default experience of language, currency, or laws.</p>
<p>There are a TON of Geolocation API services with various pricing, trustworthiness and privacy/tracking policies. I looked at a few, but the per-lookup pricing and lack of certainty around trusting a third-party with our users' IP addresses was a bit of a deterrent.</p>
<h2>Netlify and Geolocation</h2>
<p>If you haven't heard of Netlify before, it's a hosting company that specialises in JAMStack sites. I use it for this website and a lot of my personal projects, and it's a great platform for static sites, JavaScript-based frameworks and serverless/edge functions.</p>
<p>It's the serverless and edge functions that are the key to this setup. Serverless and edge functions allow us to run a node.js script on each request, responding dynamically. Serverless functions run on centralised servers (they're pretty badly named!), Edge functions are a bit more restrictive and run directly on the CDN nodes allowing for a potentially faster or lighter response.</p>
<p>These functions can be combined with <a href="https://docs.netlify.com/edge-functions/api/#netlify-specific-context-object">Netlify's <code>context</code> object</a> for geolocation information. We can send that data back on the request in a JSON format, and then use that within our front-end JavaScript.</p>
<h2>The code</h2>
<p>For my own later reference and potentially yours, I'm going through the full process of setting up a simple Edge function on Netlify!</p>
<h3>1. Initialising and installing Netlify CLI</h3>
<p>First we need to initialise our repo, npm project and install the Netlify CLI for local development.</p>
<pre class="language-sh"><code class="language-sh"><span class="token function">mkdir</span> netlify-geoip <span class="token operator">&amp;&amp;</span> <span class="token builtin class-name">cd</span> netlify-geoip<br><span class="token function">git</span> init<br><span class="token function">npm</span> init <span class="token parameter variable">-y</span><br><span class="token function">npm</span> <span class="token function">install</span> netlify-cli <span class="token parameter variable">-g</span></code></pre>
<h3>2. Trying out an edge function</h3>
<p>In Netlify projects edge functions are placed within the <code>netlify/edge-functions/</code> directory by default, so let's create an <code>netlify/edge-functions/geoip.js</code>. Within it, we're going to put the very basics of a edge function that has a text response, and specify Netlify should serve it as the root request <code>/</code>:</p>
<pre class="language-js"><code class="language-js"><span class="token comment">// netlify/edge-functions/geoip.js</span><br><span class="token comment">// Specify that this function should run on the path `/`</span><br><span class="token keyword">export</span> <span class="token keyword">const</span> config <span class="token operator">=</span> <span class="token punctuation">{</span> <span class="token literal-property property">path</span><span class="token operator">:</span> <span class="token string">'/'</span> <span class="token punctuation">}</span><br><br><span class="token comment">// We export the function that runs on each request</span><br><span class="token keyword">export</span> <span class="token keyword">default</span> <span class="token punctuation">(</span><span class="token punctuation">)</span> <span class="token operator">=></span> <span class="token punctuation">{</span><br>	<span class="token comment">// Respond to the request with the content "hello world!"</span><br>	<span class="token keyword">new</span> <span class="token class-name">Response</span><span class="token punctuation">(</span><span class="token string">'hello world!'</span><span class="token punctuation">)</span><br><span class="token punctuation">}</span><br></code></pre>
<p>To test our function, we can run <code>netlify dev</code> to run the Netlify development server. Now, if you visit the dev URL in your browser — <a href="http://localhost:8888">probably <code>localhost:8888</code></a> you should see &quot;hello world!&quot;.</p>
<h3>3. The Geolocation bit</h3>
<p>Now let's amend our <code>geoip.js</code> file to include the Geolocation bits:</p>
<pre class="language-js"><code class="language-js"><span class="token comment">// netlify/edge-functions/geoip.js</span><br><span class="token keyword">export</span> <span class="token keyword">const</span> config <span class="token operator">=</span> <span class="token punctuation">{</span> <span class="token literal-property property">path</span><span class="token operator">:</span> <span class="token string">'/'</span> <span class="token punctuation">}</span><br><br><span class="token keyword">export</span> <span class="token keyword">default</span> <span class="token keyword">async</span> <span class="token punctuation">(</span><span class="token parameter">request<span class="token punctuation">,</span> context</span><span class="token punctuation">)</span> <span class="token operator">=></span> <span class="token punctuation">{</span><br>	<span class="token comment">// The context parameters includes details about the current request,</span><br>	<span class="token comment">// including the geolocation information and client IP address</span><br>	<span class="token keyword">return</span> Response<span class="token punctuation">.</span><span class="token function">json</span><span class="token punctuation">(</span><span class="token punctuation">{</span><br>		<span class="token operator">...</span>context<span class="token punctuation">.</span>geo<span class="token punctuation">,</span><br>		<span class="token literal-property property">ip</span><span class="token operator">:</span> context<span class="token punctuation">.</span>ip<br>	<span class="token punctuation">}</span><span class="token punctuation">)</span><br><span class="token punctuation">}</span></code></pre>
<p>Once again we can test this with <code>netlify dev</code>, you may need to restart the development server to get the latest changes. If you visit the preview URL you'll get the Geolocation data and your IP address in a JSON format! Neat!</p>
<h3>4. Cross Origin Resource Sharing</h3>
<p>If we try to call this on a different website with JavaScript, we're going to run into CORS issues. CORS — Cross Origin Resource Sharing — is a way browsers prevent websites from using a browser to access content they shouldn't have access to, like resources from a local network. This means as things currently stands, a browser won't let us access the content from our API request with <code>fetch</code>.</p>
<p>To allow us to use the API within JavaScript in a browser, we need to tell the browser to allow CORS. We can do this by adding some HTTP Headers via the second argument of <code>Response.json</code>:</p>
<pre class="language-js"><code class="language-js"><span class="token keyword">export</span> <span class="token keyword">const</span> config <span class="token operator">=</span> <span class="token punctuation">{</span> <span class="token literal-property property">path</span><span class="token operator">:</span> <span class="token string">'/'</span> <span class="token punctuation">}</span><br><br><span class="token keyword">export</span> <span class="token keyword">default</span> <span class="token keyword">async</span> <span class="token punctuation">(</span><span class="token parameter">request<span class="token punctuation">,</span> context</span><span class="token punctuation">)</span> <span class="token operator">=></span> <span class="token punctuation">{</span><br>	<span class="token keyword">return</span> Response<span class="token punctuation">.</span><span class="token function">json</span><span class="token punctuation">(</span><br>		<span class="token punctuation">{</span><br>			<span class="token operator">...</span>context<span class="token punctuation">.</span>geo<span class="token punctuation">,</span><br>			<span class="token literal-property property">ip</span><span class="token operator">:</span> context<span class="token punctuation">.</span>ip<br>		<span class="token punctuation">}</span><span class="token punctuation">,</span><br>		<span class="token comment">// Add a second parameter to `Response.json`</span><br>		<span class="token comment">// where we can provide our CORS headers</span><br>		<span class="token punctuation">{</span><br>			<span class="token literal-property property">headers</span><span class="token operator">:</span> <span class="token punctuation">{</span><br>				<span class="token string-property property">'Access-Control-Allow-Origin'</span><span class="token operator">:</span> <span class="token string">'*'</span><span class="token punctuation">,</span><br>				<span class="token string-property property">'Access-Control-Allow-Methods'</span><span class="token operator">:</span> <span class="token string">'GET,OPTIONS'</span><br>			<span class="token punctuation">}</span><br>		<span class="token punctuation">}</span><br>	<span class="token punctuation">)</span><span class="token punctuation">;</span><br><span class="token punctuation">}</span><span class="token punctuation">;</span><br></code></pre>
<p>You could be more specific with your CORS headers, but for a simple API like ours this will do fine. These two lines allow all origins to access the API, and only the GET and OPTIONS methods.</p>
<p>That is one thing to note however, the <code>Access-Control-Allow-Origin</code> header allows all origins to make a request to the API. In most cases that might be okay, but you may want to prevent other sites from using your API, especially if you start hitting Netlify's usage limits.</p>
<p>You can <a href="https://developer.mozilla.org/en-US/docs/Web/HTTP/CORS#access-control-allow-origin">whitelist a single origin</a> by adding it to the <code>Access-Control-Allow-Origin</code> header instead of <code>*</code>. For multiple origins you could also dynamically read the <code>Origin</code> header and use that to allow or disallow a request. I haven't run into that problem yet though, so consider that a further exercise for the reader!</p>
<h3>5. Deploy and test!</h3>
<p>We can deploy the API to Netlify with <code>netlify deploy --build --prod</code>, or link the project via the Netlify website to a Git repo on GitHub, GitLab or similar. Now access the API at your Netlify URL, <a href="https://accudio-geoip.netlify.app">for example <code>accudio-geoip.netlify.app</code></a> and there we go!</p>
<p>This is the result I get when visiting that URL (IP obfuscated for privacy):</p>
<pre class="language-json"><code class="language-json"><span class="token punctuation">{</span><br>  <span class="token property">"city"</span><span class="token operator">:</span> <span class="token string">"Newbury"</span><span class="token punctuation">,</span><br>  <span class="token property">"country"</span><span class="token operator">:</span> <span class="token punctuation">{</span> <span class="token property">"code"</span><span class="token operator">:</span> <span class="token string">"GB"</span><span class="token punctuation">,</span> <span class="token property">"name"</span><span class="token operator">:</span> <span class="token string">"United Kingdom"</span> <span class="token punctuation">}</span><span class="token punctuation">,</span><br>  <span class="token property">"subdivision"</span><span class="token operator">:</span> <span class="token punctuation">{</span> <span class="token property">"code"</span><span class="token operator">:</span> <span class="token string">"ENG"</span><span class="token punctuation">,</span> <span class="token property">"name"</span><span class="token operator">:</span> <span class="token string">"England"</span> <span class="token punctuation">}</span><span class="token punctuation">,</span><br>  <span class="token property">"timezone"</span><span class="token operator">:</span> <span class="token string">"Europe/London"</span><span class="token punctuation">,</span><br>  <span class="token property">"latitude"</span><span class="token operator">:</span> <span class="token number">51.3195</span><span class="token punctuation">,</span><br>  <span class="token property">"longitude"</span><span class="token operator">:</span> <span class="token number">-1.4146</span><span class="token punctuation">,</span><br>  <span class="token property">"ip"</span><span class="token operator">:</span> <span class="token string">"XX.XX.XX.X"</span><br><span class="token punctuation">}</span></code></pre>
<p>It's definitely not perfect, to start I'm in Edinburgh, Scotland not Newbury, England! City and Subdivision should maybe be taken with a pinch of salt, but that's something I run into with GeoIP systems all over the web so it's clearly not just Netlify. (interestingly, my <a href="/writing/vercel-geoip">Vercel post</a> had similar but slightly different results)</p>
<p>For the purposes of country though it's accurate, and the City and Subdivision may be helpful to set a default that a user can later change.</p>
<h3>6. Using the API within JavaScript</h3>
<p>We can use this within JavaScript on another website like so, but keep in mind you may need to switch from using <code>await</code> to <code>.then()</code> depending on your setup.</p>
<pre class="language-js"><code class="language-js"><span class="token keyword">const</span> geoRequest <span class="token operator">=</span> <span class="token keyword">await</span> <span class="token function">fetch</span><span class="token punctuation">(</span><span class="token string">'https://accudio-geoip.netlify.app'</span><span class="token punctuation">)</span><br><span class="token keyword">const</span> geo <span class="token operator">=</span> <span class="token keyword">await</span> geoRequest<span class="token punctuation">.</span><span class="token function">json</span><span class="token punctuation">(</span><span class="token punctuation">)</span><br><br>console<span class="token punctuation">.</span><span class="token function">log</span><span class="token punctuation">(</span>geo<span class="token punctuation">.</span>country<span class="token punctuation">.</span>code<span class="token punctuation">)</span><br><span class="token comment">// GB</span></code></pre>

      <hr>
      <p>If you have any comments or feedback on this article, let me know! I'd love to hear your thoughts, go ahead and <a href="mailto:alistair@accudio.com">send me an email</a> at alistair@accudio.com or <a href="https://front-end.social/@accudio" rel="noopener noreferrer">contact me on Mastodon</a>.</p>
      <p>The post <a href="https://www.alistairshepherd.uk/writing/netlify-geoip/">Simple, cheap GeoIP API using Netlify Edge functions</a> appeared first on <a href="https://alistairshepherd.uk">alistairshepherd.uk</a>.</p>
    ]]>
      </content>
    </entry>
  
    
    <entry>
      <title>Bone conduction earphones revolution — Aftershokz Aeropex review</title>
      <link href="https://www.alistairshepherd.uk/writing/aeropex-review/"/>
      <updated>2023-07-30T00:00:00Z</updated>
      <id>https://www.alistairshepherd.uk/writing/aeropex-review/</id>
      <content type="html">
        <![CDATA[
      <p>It's been a very long time since I've been really excited about some new technology.</p>
<p>I used to be thrilled over everything new, but in the past decade or so it's just faster, smaller, higher pixel density. VR got kinda close but still feels a bit like strapping a phone to my face.</p>
<p>That's why I was so surprised when a new pair of earphones completely blew my fucking mind. Those earphones are the Aeropex by Aftershokz and they're a pair of <em>fantastic</em> bone conduction earphones. I don't do reviews but my amazing experience with these meant I had to share it.</p>
<p>Previously when on video calls at work I used a pair of open-backed headphones or a pair of fairly standard earbuds. Now I'm quite a loud person normally, and it turns out that when I talk with headphones/earbuds in I get even louder, almost shouting. Enough that when I starting having more calls my partner complained and a neighbour mentioned it. 😬</p>
<p>I'd heard of bone conduction earphones before and they seemed like a decent option. If you're not familiar with them, they have a couple of pads that sit against your temples and get sound into your brain by 'conducting' it through the bone. It then travels to your eardrum through the bone and you hear it as if it was normal sound coming through the air. This leaves your ears and ear canal clear for environmental noise and for your own voice, importantly for me.</p>
<p>TLDR: the Aeropex are amazing, blew my mind when I first started using them and would recommend them to everyone. They're comfortable, sound great, are easy to use and hold a decent battery. I've used them for about 4 months now, they've replaced all of the other earphones/headphones I had previously, and I don't think I'll ever go back to other headphones/earphones.</p>
<h2>Buying, unboxing etc</h2>
<p>I bought them on discount for £90 from Amazon, the Aeropex no longer seem to be on Amazon but they're available for similar prices elsewhere and the OpenRun on Amazon looks pretty similar.</p>
<p>It was in very professional and nice packaging, although a bit on the over-engineered side. Standard sort of thing for tech products. The box included the earphones themselves — pre-charged to a pretty full battery — a silicone carrying case I'll never use, disposable foam earbuds (I guess for if you actually do want to block out noise?) and two charging cables.</p>
<p>The earphones themselves are pretty small and light, so don't have a USB or whatever port and instead has a custom magnetic two-contact charging connection. The charging cable is USB A on one end and just snaps in place when close to the earphones. Having a second cable is a nice touch, I wouldn't want to lose one and not be able to easily get a replacement.</p>
<h2>Setting up, using them, and the fit</h2>
<p>They pair via Bluetooth, a pretty standard 'hold the button til you hear the beep' sort of setup. They seem to be able to pair with any number of devices — so far I'm at 7 — and to switch you just disconnect from one device and reconnect to another. They'll automatically connect to the last device used which can sometimes be annoying — my laptop on sleep the other side of the house will connect to it instead of my phone right next to me. The connection is really solid and has never really had issues except when the device and earphones have been separated by a good 10m or a few walls.</p>
<p>Aftershokz claim the battery life is 8 hours but I found it was consistently a lot better than that. For me they last about 10-15 hours of near-constant use, and generally does me 2-3 days before needing a recharge. They recharge very quickly, needing only 30 mins or so from dead to full.</p>
<p>On battery, there are audio warnings about the battery level when you turn on/off and as you approach empty. They're not super useful though really, I find &quot;High&quot; lasts a couple hours, &quot;Medium&quot; lasts ~5 hours and &quot;Low&quot; lasts 5+ hours. So I hear &quot;Low&quot; and have no idea if that means 'need to charge tonight' low or 'about to shut down' low. Android, Windows and Mac all report the percentage in increments of 10% pretty accurately though so check that instead.</p>
<p>They go easily over your ears and behind the back of your head, holding themselves pretty well but not tightly. They're very flexible so will likely fit most heads okay. They're super easy for me with fairly short hair, but as a friend discovered they're a bit trickier for longer and curlier hair — you may need to put them between your hair and back of your neck.</p>
<p>They're probably the most comfortable personal audio device I've had, as they're very light and just rest on your ears with no pressure. They do compete for space above my ears with my glasses but not enough to be uncomfortable, I just need to make sure they're on the outside otherwise my glasses fall off. I fairly regularly leave them on even if they're turned off as I either forget I'm wearing them or it's more convenient than carrying them.</p>
<p>Buttons are fine, a simple play/pause on the left side makes it super easy to pause if needed and volume+power controls are tucked behind the right ear. The buttons have multiple functions depending on how you press them but all I really use are power on/off, volume, and play/pause.</p>
<h2>The sound and in different uses/environments</h2>
<p>How good do they sound? Great. A lot of criticism of bone conduction earphones is that they typically don't sound amazing. Bone through skin is harder to vibrate than air I guess!</p>
<p>But these really do sound good. I'm quite into music and have some decent audio kit, talking a few hundred quid of DAC, AMP and speakers at both my desk and living room, some studio-quality headphones and ~£200 wireless earbuds. Even then I can only really critique the quality of the Aeropex when directly comparing them. They're not going to win awards for best sound, but in isolation I find it quite difficult to judge. I've heard many 'traditional' earbuds and headphones from audio companies that sound worse despite costing more.</p>
<p>Getting into more detail, they sound even and well-balanced but lacking some bass. The high frequencies aren't sharp which I love as I'm quite sensitive to high frequencies. Lower frequencies apparently don't conduct as well from the bone to the eardrum which is why they don't come through as well. It's not bad or very noticeable a lot of the time, but for some bass-heavy songs they do lack a bit of oomph.</p>
<p>The way my brain placed sound coming through bone did take some getting used to. Rather than coming from my ears/externally it sounds a bit like the music is actually coming from inside of my head, somewhere between my eyes. It didn't take long for that to be normal however. I suspect this actually helps with my auditory processing issues as it means external noise very obviously comes from a different direction!</p>
<p>The volume controls are just right, the top end is loud but not extremely so. You wouldn't want full volume all the time but are okay for a short period in a loud environment. It can go very quiet if you're sensitive to loud environments and are in almost-silent environments. There's 15 different volume levels so a good amount of control, I tend to keep mine around 7-8 depending on the environment.</p>
<p>The way they sit next to your ears means the pads are sitting right on top of my sideburns. Amusingly that means I do notice a difference in audio quality depending on how unruly my sideburns are! If they have worse contact the quality isn't quite as good so keep that in mind if you have crazy sideburns. My mother would be thrilled to hear I have to keep them better trimmed.</p>
<p>As I mentioned earlier, I use these basically everywhere now. Since I've had people ask 'how do they manage in &lt;circumstance&gt;', here's a summary for you:</p>
<h3>Work video calls</h3>
<p>Great. I can manage my voice volume and hear my doorbell fine better than open-cup headphones and without speakers. No more complaints from my partner about shouting when on a call!</p>
<h3>Around the house</h3>
<p>I love them whilst cooking in particular, can hear my environment perfectly yet still listen to music/podcasts. Have to crank them up when the extraction fan is on.</p>
<h3>Very loud activities</h3>
<p>One area they do not do well. I bust out my old earphones for music whilst I hoover or use loud equipment.</p>
<h3>Out and about</h3>
<p>Generally good in all but the busiest and loudest circumstances. Music works fine but sometimes I need to rewind an audiobook/podcasts when next to a very busy road or on a Saturday afternoon in Edinburgh centre.</p>
<h3>Hill walking</h3>
<p>Ideal! As it's generally a pretty quiet environment they do well, and as my ears are still free I can still hear people, vehicles and animals with no problems. I used to have only one earphone in but now I get stereo music and stereo environment!</p>
<h3>Exercise/activity</h3>
<p>They sit securely (even when I shake my head) and are IP67 water-resistant so you don't need to worry about rain or sweat. I don't run but lots of reviews say they're fantastic for running.</p>
<p>Similarly to hill walking, it's very handy being more aware of your environment and people around you. When using them skiing I really appreciate not having to rip out an earbud when about to hop on a lift!</p>
<p>For activities with a helmet the main thing to be concerned about will be the way they go around the back of your head. For me it sits pretty low, hovering around the back of my neck and works fine with my ski helmet, but something to keep in mind if you wear a bigger helmet.</p>
<h2>Microphone</h2>
<p>The microphone definitely isn't anything to call home about 😉. I tend to use the mic of the connected device as it tends to be a bit better, but the in-built one does work if needed.</p>
<p>One issue I've had is when used with Windows or MacOS the headphones go into 'headset mode' when a call software uses them for audio output and input. This reduces the output quality significantly and stops the mic from working completely in Google Meet. The easiest solution I've found for this is to use a different mic. In Windows you can set something else as default, in Mac you have to create an 'Aggregate device' using your other mic as otherwise the headset is set to the default every time it connects.</p>
<h2>Final thoughts</h2>
<p>Honestly I'd recommend these to everyone but the most particular of audiophiles. Many people say these are particularly for running or whatever but I think these are perfect for so much more. On some days I am wearing them about 75% of my waking hours and they continue to be comfortable, great to listen to and keeping me aware of my environment.</p>
<p>If I was scoring it I would give it a 9/10, just missing a 10 thanks to the vague battery level warnings and issues with the microphone on calls until I worked that out. Thank you for reading the first and possibly the last of my tech review series!</p>

      <hr>
      <p>If you have any comments or feedback on this article, let me know! I'd love to hear your thoughts, go ahead and <a href="mailto:alistair@accudio.com">send me an email</a> at alistair@accudio.com or <a href="https://front-end.social/@accudio" rel="noopener noreferrer">contact me on Mastodon</a>.</p>
      <p>The post <a href="https://www.alistairshepherd.uk/writing/aeropex-review/">Bone conduction earphones revolution — Aftershokz Aeropex review</a> appeared first on <a href="https://alistairshepherd.uk">alistairshepherd.uk</a>.</p>
    ]]>
      </content>
    </entry>
  
    
    <entry>
      <title>Alternative text for &quot;I&#39;m a Luddite (and So Can You!)&quot;</title>
      <link href="https://www.alistairshepherd.uk/writing/im-a-luddite-alt-text/"/>
      <updated>2023-08-02T00:00:00Z</updated>
      <id>https://www.alistairshepherd.uk/writing/im-a-luddite-alt-text/</id>
      <content type="html">
        <![CDATA[
      <p>I recently enjoyed the web comic <a href="https://thenib.com/im-a-luddite/">&quot;I'm a Luddite (and So Can You!)&quot; by Tom Humberstone</a>. It's a fantastic history and summary of the Luddites and how in the modern world you should also be a Luddite.</p>
<p>Unfortunately, it doesn't have any alternative text on any of the images so is inaccessible. To make this accessible to more people and <a href="https://mastodon.scot/@tomhumberstone/110809971374272258">with Tom Humberstone's blessing</a>, I've written out the alternative text here. I've written it as one list item per panel, please see <a href="https://thenib.com/im-a-luddite/">the original comic</a> for the images.</p>
<h2>Alternative text</h2>
<ol>
<li>Two people using sledgehammers to break a laptop, with keyboard keys flying everywhere. One is wearing an apron, the other a waistcoat.</li>
<li>Background of futuristic city. &quot;The future is here already. AI art has arrived. Simply write a prompt...&quot;</li>
<li>Selfie above Upload file button. &quot;Or allow a company unrestricted access to your likeness...&quot;</li>
<li>The same selfie but transformed by AI, has lots of fingers, face with horrible teeth and many eyes, a ridiculous number of fingers and buttons on shirt and a patterned background. &quot;...And ta-da! Some absolutely serviceable hotel lobby art. But calling this 'AI art' uncritically buys into the AI hype machine.&quot;</li>
<li>A digital hand stealing a piece of art from the wall in front of two alarmed people. &quot;'Algorithmically generated art theft' might be more accurate.&quot;</li>
<li>Nonsensical pen scribble. &quot;Machine-learning software is 'taught' by feeding it existing art without consent, credit, or compensation. To the extent that mangled signature remnants remain visible on AI art.&quot;</li>
<li>Protest of people with signs with AI crossed out. &quot;Artists have begun to push back&quot;. Man saying: &quot;Everybody who creates for a living should be in code red.&quot; &quot;Matthew Butterick, co-counsel in a class action lawsuit brought against AI art by artists&quot;</li>
<li>caption: &quot;A common refrain from defenders of AI art has been to label these naysayers:&quot; with four speech bubbles: &quot;Luddite!&quot;, &quot;Luddites!&quot;, &quot;Luddite!&quot;, &quot;Luddism!&quot;</li>
<li>Four people having a meal around a table. &quot;---a term synonymous with technophobia, anti-progress and reactionism. It's even used to describe being hapless with new tech.&quot; One of the people says &quot;Oh, I can't use TikTok! I'm such a Luddite!&quot; Further caption: &quot;In truth, the Luddites were skilled with machines. They were simply fighting for better works rights.&quot;</li>
<li>&quot;In 1799, the British government passed legislation that prohibited trade unions and collective bargaining.&quot; a old-timey-looking aristocrat signing with a quill with mill-workers in the background looking at a mechanised loom. &quot;Mill owners introduced more machines in their factories, reducing wages&quot;</li>
<li>Man swinging a sledgehammer. &quot;And so, in 1811, after years of frustrating negotiations, a spate of coordinated attacks on mill frameworks erupted across the United Kingdom, lef by 'King Ned Ludd'&quot;.</li>
<li>Warrior with a stick, protests and burning buildings in the background. &quot;Ludd was an elusive figure, spotted with 'a pike in his hand' and a ghostly pale face. Which is surprising. Because Ned Ludd didn't exist.&quot;</li>
<li>&quot;He was a fiction. A symbolic figure whose 'existence' made it hard for the authorities to target leadership and encouraged decentralized campaigning.&quot; Two grinning men cheers a drink.</li>
<li>&quot;The Luddites' methods included letter-writing campaigns petitioning for better wages and  the end of child labor.&quot; An older man with white hair sits between two huge stacks of letters.</li>
<li>&quot;Their concerns earned them solidarity from local communities.&quot; Well-dressed man in front of large crowd of people saying &quot;You may call the people a mob, but do not forget that a mob too often speaks the sentiments of the people.&quot; Captioned &quot;Lore Byron in his first speech to Parliament. Mill owners were terrified. Wages rose. Conditions improved.&quot;</li>
<li>Soldiers with muskets, papers strewn everywhere, a gallows ominously slightly off-frame. &quot;But then 14,000 soldiers were sent to protect the mills and quash riots. New legislation made frame-breaking a capital offense. Eventually, Key Luddite organizers were identified, arrested and executed.&quot;</li>
<li>&quot;Yet the movement lived on, Outbreaks of machine-breaking continued for years in the UK and beyond.&quot; Man saying &quot;This is dew, Which the tree shall renew Of Liberty, planted by Ludd!&quot; Captioned &quot;Lord Bryon, 'Song for the Luddites'&quot;</li>
<li>Woman swinging sledgehammer and breaking something. &quot;In 1837. a spinning jenny in Chalabre, France, was destroyed by workers. The women workers 'made themselves conspicuous by their fury and violence.' From local newspaper L'Aude&quot;</li>
<li>&quot;In the 1870s, English textile designer, author and socialist William Morris began taking an interest in the production process of his designs. he was disgusted by the poor living conditions of workers and the pollution caused by the textile industry.&quot; A man in a suit says: &quot;Why does a resonable man use a machine? Surely to save his labour. Under capitalism, machines were primarily used to increase production, thereby increasing the worker's drudgery.&quot;</li>
<li>&quot;People has been making similar arguments against AI art.&quot; Tweet from &quot;Cat Bastard Quinn @QuinnCat13&quot;: &quot;We could automate menial jobs so people have time to make art and music but apparently we'd rather automate art and music so people have time for menial jobs.&quot;</li>
<li>&quot;Likewise, Morris was interested in what he called 'worthy work'. he wanted people to take pleasure in their work rather than 'mere toiling to live, that we may live to toil.'&quot; Woman frowning in front of a laptop and a man working in an Amazon warehouse.</li>
<li>&quot;He understood that machines were only as progressive as the people who used them.&quot; Man in suit in front of loads of notification badges says &quot;[An unfree man is a] slave the machinery; the new machine MUST be invented, and when invented he MUST - I will not say se it, but be used by it, whether he likes it or not.&quot;</li>
<li>Workers working on a car engine, man in suit looking at pocket watch in front. &quot;In America, mechanical engineer Frederick W. Taylor began using what he called the 'Scientific Management' in his factories. Taylor timed each worker's every movement, breaking down their work into a set of discrete tasks. Then demanded workers speed them up.&quot;</li>
<li>&quot;Taylorism,&quot; as it became known, was less a science than a political ideology concerned with remolding workers into pliant subjects.&quot; Workers on production line looking frustrated, Taylor in front says &quot;A complete mental revolution on the part of the workingman... toward their work, toward their fellow men, and toward their employers.&quot;</li>
<li>&quot;But the spirit of Luddism lived on. Seemingly out of nowhere, there was a rash of mechanical breakdowns at Taylor's factories.&quot; Two workers shrug in front of smoke. Taylor, looking angry, says &quot;These men were deliberately breaking their machines.&quot;</li>
<li>&quot;Despite setbacks and intervention from Congress, Taylorism spread.&quot; Pictured is a book &quot;Sabotage, it's history, philosophy and function&quot; by Walker C Smith, with the cover of a black cat opening a bag of money. Captioned &quot;The Industrial Workers of the World responded by publishing two tracts on the topic of sabotage in 1913. 'The aim is to his the employer in his vital spot, his heart and soul, in other words, his pocketbook.'&quot;</li>
<li>Hands on a conveyer of plates, written on each: &quot;'Automation' — as it was later dubbed by Delmar Harder, vice president on Ford Motor Company, in 1947 — continued unabated.&quot;</li>
<li>&quot;The legacy of the Luddites lived on in the latter half of the 20th century. Confined to the lowest-paying jobs, black workers were the first to be targeted by the midcentury push for automation.&quot; Two black woman work on riveting a piece of metal. A black man in the foreground says &quot;Not only is the economic situation of the masses of blacks grim, but the prospects are that it wil not improve, rather it will deteriorate. This is due partley to the unregulated impact of automation.&quot; Captioned &quot;Robert L. Allen, Black Awakening in Capitalist American (1969)&quot;</li>
<li>&quot;The Black Panthers&quot; were also quick to recognize technology and automation were not politically neutral.&quot; A black man at a microphone says &quot;If the ruling circle remains in power it seems to me that capitalists will continue to develop their technological machinery because they are not interested in the people... Every worker is in jeopardy because of the ruling circle.&quot; Captioned &quot;Huey Newton, cofounder of the Black Panther Party&quot;</li>
<li>&quot;The Black Panether Party's Ten Point Program was updated in 1972. 'People's community control of modern technology' was added to the demands for 'land, break, housing, education, clothing, justice, peace'&quot;. A group of black people and workers stand in the background.</li>
<li>&quot;In 2019 McKinsey Global Institute found that: 'African Americans are overrepresented in occupations likely to be most affected by automation.'&quot; A chart titled &quot;Overrepresentation of African Americans in 3 occupation categories with the highest expected displacement, %&quot; shows: Office support eg secretaries 36%; Food services eg fast-food cooks 35%; Production work eg machinists 34%. Captioned &quot;And the 2030 outlook doesn't look much better.&quot;</li>
<li>&quot;The introduction of the computer in the 1970s only exacerbated growing tensions. Punch cards used in universities, similar to cards used for drafting recruits for Vietname, were seen as a symbol of bureaucracy and alienation. Students burned, vandalized, and otherwise destroyed punch cards for course registration.&quot;Pictured is a woman using a machine, a protext of students, someone holding a punch card labelled 'STRIKE' and a badge saying &quot;I am a human being: do not fold, spindle or mutilate&quot;</li>
<li>Person saying &quot;Many people simply stopped drawing distinctions between one card-enabled system and another. Whether the cards registered draftees or pupils, they helped 'the system'.&quot; Captioned &quot;Lisa Gitelman, author of Always Already New: Media, History, and the Data of Culture (2008)&quot;</li>
<li>&quot;Vietnam had become the first computational war, bringing a shift towards strategies rooted in quantitative data collection and automated analysis.&quot; pictured are some men working computer terminals and banks of controls with large screens.</li>
<li>&quot;Sensor arrays and unmanned drones became a big part of the war. An ongoing experiment with the intention of the eventual replacement of human pilots. American soldiers were sabotaging equipment, staging protests, and refusing to fight. 'Automation of war', as with the automation of industry, was a political strategy designed to, once again, reassert control over rebellious workers.&quot; pictured alongside a Tom Cat drone</li>
<li>&quot;Now as more of our daily lives are automated, people are finding that it doesn't always make our lives easier.&quot; A man looks confused at a self-service supermarket till as it has a big red X and says &quot;Unexpected item in the bagging area!&quot;</li>
<li>&quot;In 2016, a study revealed that physicians spend two hours of computer work for every hour spent speaking with a patient face to face.&quot; Two medical works in the background are frustrated at a computer. In the foreground a man says &quot;I'e come to feel that a system that promised to increase my mastery over my work has, instead, increased my work's mastery over me.&quot; Captioned &quot;Atul Gawande, surgeon and public health researcher&quot;</li>
<li>&quot;In the rush to claim success with new AI software, this invisible human work becomes even more insidious. Researcher Jathan Sadowski calls it 'Potemkin AI'.&quot; Three people use computers with red screens, man in foreground says &quot;There is a long list of services that purport to be powered by sophisticated software but actually rely on humans acting like robots.&quot;</li>
<li>&quot;Writer and filmmaker Astra Taylor calls these modern-day Mechanical Turks an example of 'fauxtomation'.&quot; A woman says &quot;[Fauxtomation] reinforces the idea that work has no value if it is unpaid and acclimates us to the idea that one day we won't be needed.&quot;</li>
<li>&quot;As a 2019 report by the think tank Data &amp; Society concludes: Automated and AI technologies tend to mask the human labour that allows them t obe fully integrated into a social context while profoundly changing the conditions and quality of labour that is at stake.&quot; Pictured alongside a woman taking off a robot-like mask</li>
<li>&quot;Opposition to 21st-centure tech can be found in unlikely places. Silicon Valley executives are restricting their own children's screen time and sending them to tech-free schools.&quot; A quaint small &quot;Waldorf School&quot; has greenery outside it and a man says &quot;You can't put your face in a device and expect to develop a long-term attention span.&quot; captioned &quot;TaeWoo Kim, chief AI engineer at maching learning startup One Smart Lab.&quot;</li>
<li>&quot;In San Francisco, security robots sent to harass the homeless has been repeatedly assaulted, with one being covered in BBQ sauce and wrapped in tarp.&quot; A round, pillar-like security robot is covered in a dark liquid.</li>
<li>&quot;In Arizona, people are slashing the tires of driverless cards after a driverless Uber struck and killed a woman in Tempe.&quot; A driverless car has a smashed window. &quot;People are lashing out justifiably. There's a growing sense that the giant corporations honing driverless technologies do not have our best interests at heart.&quot; by &quot;Douglass Rushkoff, author of Team Human (2019)&quot;</li>
<li>Pie chart showing 85%. &quot;A Pew research poll found that 85% of Americans favored the restriction of automation to only the most dangerous forms of work.&quot;</li>
<li>&quot;We're all living through an era in which we have become the product.&quot; People in the background are on phones and have $ signs above their heads. A woman in the foreground says &quot;The age of surveillance capitalism is a titanic struggle between capital and each one of us. It it a direct intervention into free will, an assault to human autonomy. To tune and herd and shape and push us in the direction that creates the highest probability of their business success. [There's no way] to dress this up as anything but behavioural modification.&quot; Captioned &quot;Shoshana Zuboff, author of The Age of Surveillance Capitalism (2018)&quot;&quot;</li>
<li>&quot;Many are also drawing attention to the biases of software made by an almost entirely male, predominantly white workforce&quot; A black man's face is being scanned and there's a big red &quot;Error&quot; sign next to him. &quot;I'm worried about groupthink, insularity, and arrogance in the AI community. [...] If many are actively excluded from its creation, this technology will benefit a few while harming a great many. Timnit Gebru, Black in AI cofounder&quot;</li>
<li>&quot;VPNs, the dark web, and plugins like RequestPolicy are arguably Luddite responses to new technology&quot;. A man says in front of set of red security symbols: &quot;A retrograde attempt to rewind web history: a Luddite machine that, as they say 'breaks' the essential mechanisms of websites.&quot; Captioned &quot;Computer science students have already developed Glaze - a tool to precent AI models from mimicking artist styles. Maxigas, Resistance to the Current: The Dialectics of Hacking (2022)&quot;</li>
<li>Man saying &quot;Luddism contains a critical perspective on technology that pays particular attention to technology's relationship to the labour process and working conditions. In other words, it views technology not as neutral but as a site of struggle, Luddism rejects production for production's sake: it is critical of 'efficiency' as an end goal.&quot; Captioned &quot;Gavin Mueller, Breaking Things as Work (2021)&quot;</li>
<li>&quot;Degrowth, Slow Living, Quiet Quitting and the I Do Not Dream of Labour movements could all be described as forms of modern neo-Luddism.&quot; Someone tears apart a sheet showing an upward-trending graph. &quot;Lashed to the growth imperative, technology is used not to do the same amount of stuff in less time, but rather to do more stuff in the same amount of time. Jason Hickel, author of Less Is More: How Degrowth Will Save the World (2020)&quot;</li>
<li>A man rests a sledgehammer on one shoulder, looking away with trees and rolling hills in the background. &quot;Questioning and resisting the worst excesses of technology isn't antithetical to progress. In your concept of 'progress' doesn't put people at the center of it, is it even progress? Maybe those os us who are apprehensive about AI art are Luddites. Maybe we should wear that badge with pride. Welcome to the future. Sabotage it.&quot;</li>
<li>&quot;From the Future Issue&quot;</li>
</ol>

      <hr>
      <p>If you have any comments or feedback on this article, let me know! I'd love to hear your thoughts, go ahead and <a href="mailto:alistair@accudio.com">send me an email</a> at alistair@accudio.com or <a href="https://front-end.social/@accudio" rel="noopener noreferrer">contact me on Mastodon</a>.</p>
      <p>The post <a href="https://www.alistairshepherd.uk/writing/im-a-luddite-alt-text/">Alternative text for &quot;I&#39;m a Luddite (and So Can You!)&quot;</a> appeared first on <a href="https://alistairshepherd.uk">alistairshepherd.uk</a>.</p>
    ]]>
      </content>
    </entry>
  
    
    <entry>
      <title>Interop Priority Game 2024</title>
      <link href="https://www.alistairshepherd.uk/writing/interop-priority-game/"/>
      <updated>2023-11-17T00:00:00Z</updated>
      <id>https://www.alistairshepherd.uk/writing/interop-priority-game/</id>
      <content type="html">
        <![CDATA[
      <p>The other day <a href="https://bkardell.com/">Brian Kardell</a> asked about <a href="https://github.com/web-platform-tests/interop">Interop project</a> prioritisation on his blog and mastodon. As he asks in his blog <a href="https://bkardell.com/blog/PriorityGame.html">&quot;Let's Play a Game&quot;</a>:</p>
<blockquote>
<p>It's Interop 24 planning time! Let's play a game: Tell me what you prioritize, or don't and why?</p>
</blockquote>
<p>The Interop Project is an collaboration across the browser communities to focus on making various web APIs interoperable, standard, and bug-free across the Blink, Webkit and Gecko engines. It's been very productive over the past few years to fix interop bugs and to release new features like container queries and subgrid in a co-ordinated way.</p>
<p>Each year, the Interop Project accepts proposals for what should be included in the project, and once whittled down carefully considers how to prioritise working on these various important or exciting proposals.</p>
<p>So Brian has come up with a game! Ask developers to look through the full list, sort some of them and maybe explain a little bit of why they made the decisions they have. I found this a really interesting exercise and a great way of getting up-to-date with some of the things upcoming to the web platform. Please consider looking at the proposals, voting to what you like the look of and maybe doing your own prioritisation like this!</p>
<h2>My top fifteen interop proposals</h2>
<p>My order comes from my personal priorities, which are in Accessibility, Performance, and making the kind of websites I work on easier/more fun to build. Those are mostly public, lightweight websites with a bit of creativity. I don't know all the proposals super in-depth but I think enough to make a quick judgement of &quot;oh yeah I want that&quot;.</p>
<p>There are over 90 proposals in total and pretty much all of them seem like they would be useful — it makes sorting them tricky! The &quot;game&quot; is to prioritise them to your own order, and if you have your own opinions make your own list!</p>
<h3>1. <a href="https://github.com/web-platform-tests/interop/issues/568">display: contents accessibility</a></h3>
<p>All of the accessibility-related improvements and fixes hit the top of my list. I work in accessibility, that was always going to happen and I personally feel like stuff that is &quot;nice to have&quot; for developers should come after reducing barriers and making the web more accessible. This one is both nice to have and an accessibility improvement so tops the list.</p>
<p><code>display: contents</code> is a very useful CSS property that allows an element to basically remove it's own element box and allow it's child nodes to participate in a higher level. That's particularly handy for wrapping an element in a semantic container—like <code>li</code>—but allowing children to sit within the grid or flex layout of the parent.</p>
<p>Unfortunately, it's hampered by buggy and unreliable accessibility. In several browsers using <code>display: contents</code> will remove an element from the accessibility tree and therefore is best to avoid. It's my first priority as it's a great feature that is currently unusable, and an accessibility issue.</p>
<h3>2. <a href="https://github.com/web-platform-tests/interop/issues/512">Accessibility issues with display properties</a></h3>
<p>Another accessibility one, and this is related to setting <code>display</code> properties on particular elements. In some circumstances changing the display property of certain elements can remove or break accessibility for those elements. These circumstances are all over the web, and it would be good to ensure people relying on accessibility technology aren't impacted negatively.</p>
<h3>3. <a href="https://github.com/web-platform-tests/interop/issues/526">Accessibility (computed role + accname)</a></h3>
<p>The final accessibility proposal in Brian's list, this is standardising and making accessibility roles and names more consistent. It'll make it easier to build complex interfaces in an accessible way, and improve the experience for people using accessibility technology. Sounds great.</p>
<h3>4. <a href="https://github.com/web-platform-tests/interop/issues/437">View Transitions Level 1</a></h3>
<p>View Transitions are absolutely amazing, and I think they're going to a monumental addition to the web. In short, page transitions but also so much more! Particularly for building flashy, creative websites it makes it easier to produce something that feels great without needing to ship a huge amount of JS to do so.</p>
<p>Now this proposal is for Level 1, so only the 'SPA API' that does same-document transitions. Cross-document transitions are in the Level 2 module which is still in draft. That said, there's a lot of amazing things you can do with the SPA API and by getting Level 1 out it'll encourage work on Level 2 and I want that as soon as possible!</p>
<h3>5. <a href="https://github.com/web-platform-tests/interop/issues/430">JPEG XL image format</a></h3>
<p>JPEG XL looks to be a fantastic upcoming image format with great high-fidelity compression, fast encoding/decoding, backwards compatibility with JPEG, and lots of great image format features that make it look like a great candidate for the canonical image format for most-use cases.</p>
<p>From the perspective of web performance I'm really excited for the potential of JPEG XL.</p>
<h3>6. <a href="https://github.com/web-platform-tests/interop/issues/439">Scroll-driven animations</a></h3>
<p>I've been playing with Scroll Driven animations recently and found them a great API that makes it super easy to implement really cool animations linked to the scroll, with performance that's almost impossible to achieve otherwise and only a handful of lines of CSS.</p>
<p>I've worked on projects with complex scroll animations that are basically impossible to maintain. The mess of timelines in JS are a nightmare, and the CSS API and use of native <code>@keyframe</code> animations is fantastic in comparison. From a performance perspective it also removes the need to reach for a 100+kB JS library for a simple scroll effect.</p>
<h3>7. <a href="https://github.com/web-platform-tests/interop/issues/529">Text fragments</a></h3>
<p>Now this one is surprisingly high! Text fragments make it possible to <a href="#:~:text=Now%20this%20one%20is%20surprisingly%20high!">link to certain text from a page and highlight it</a>, using a format like <code>#:~:text=Now%20this%20one%20is%20surprisingly%20high!</code>. Fairly regularly I want to link someone to a specific part of a page/article, but there are no in-built ID anchor links nearby. With this I can manually construct a URL that links straight to where I want it.</p>
<p>I already use it to link people who I know are using chromium-based browsers, but would love for it to be possible in all browsers.</p>
<h3>8. <a href="https://github.com/web-platform-tests/interop/issues/520">CSS Multi-Column Layout block element breaking</a></h3>
<p>I feel like issues and bugs with multi-column layout have been a constant through my career in web since Chrome added support in 2016. Every other time I try and use them I give up and instead use a less ideal Grid, Flex or JS-based layout. Or tell the designer &quot;Sorry, the web can't do columns properly&quot; which is completely ridiculous.</p>
<p>It's about time CSS multi-column is sorted so it's reliable enough to use consistently.</p>
<h3>9. <a href="https://github.com/web-platform-tests/interop/issues/513">Unit division and multiplication for mixed units of the same type within calc()</a></h3>
<p>Currently in the CSS <code>calc</code> function division can only be done by unitless numbers. If we were able to divide by value with a unit it would open the way to strip units and to compare the scale of values with different units.</p>
<p>This isn't one I run into often—hence it's position at 9 there's been a handful of times it's come up as something that would make CSS SO much easier. There is a cool but nasty hack using <code>tan(atan2())</code> but otherwise the workaround are annoying and either involve duplication or JS.</p>
<h3>10. <a href="https://github.com/web-platform-tests/interop/issues/422">text-box-trim</a></h3>
<p>Text box trim allows trimming the space around text, so you can rely on padding and margins to sit flush with the text glyphs. In some designs you want a really neat alignment between a heading and graphic, currently that's tricky without resorting to fiddling with line-heights or <a href="https://css-tricks.com/magic-numbers-in-css/">&quot;magic numbers&quot;</a>.</p>
<p>In a design-led agency, this is definitely something our designers are looking forward to and would make heading design more flexible and easier.</p>
<h3>11. <a href="https://github.com/web-platform-tests/interop/issues/433">CSS style container queries</a></h3>
<p>If you need to change multiple properties in CSS at once with something manual, your best bet is adding or removing CSS classes. Which requires server-side or JS logic, and can get a real mess. Style container queries allows modifying CSS properties depending on the value of a single custom property. Basically a custom if statement within CSS.</p>
<p>This is an awesome feature and super handy. Despite that, it's lower down this list for me as it doesn't really solve problems I have with the utility-first CSS methodology I use at work.</p>
<h3>12. <a href="https://github.com/web-platform-tests/interop/issues/420">CSS Nesting</a></h3>
<p>This is another one that is super exciting for certain methodologies and ways of writing CSS, but it doesn't really match up with how I do. At work I write utility-first CSS that doesn't really benefit from nesting, and on my own projects I lean hard into BEM-style nesting which needs pre-processed. Ah, how I wish they'd added BEM-style nesting natively but no cigar.</p>
<p>It's extremeley powerful, a great addition to the language, and I know some people super excited for this. Maybe I will be if I have another look at how I structure CSS, but that's why it's not top of the list.</p>
<h3>13. <a href="https://github.com/web-platform-tests/interop/issues/517">CSS background-clip</a></h3>
<p>This is particularly around inconsistencies with how clipping text works with background-clip. Clipping backgrounds to text is a super neat feature that can produce some really cool looking effects, particularly combined with images. It can be pretty finicky though so it would be great if it were more consistent.</p>
<h3>14. <a href="https://github.com/web-platform-tests/interop/issues/562">text-wrap: pretty</a></h3>
<p>A way of preventing widows and generally improving readability across lines in paragraphs. Sounds good, and people who care about typography will love it. Definitely handy, I'll use it when it's available, but it's a relatively minor issue to me.</p>
<h3>15. <a href="https://github.com/web-platform-tests/interop/issues/561">text-wrap: balance</a></h3>
<p>Finally we have <code>text-wrap: balance</code>, which provides a better layout for short blocks of text considering where to break lines. Just adding it to major headings can easily make them look a little nicer. Like <code>text-wrap: pretty</code>, it'll be handy and I'll use it but I'm not clamouring for it!</p>
<h2>Honourable mentions</h2>
<p>These are all on my radar and of interest, but less of a priority. That may be because I don't know enough, I don't use them, am unsure quite where to place them, or they have less convenient but full-featured alternatives:</p>
<ul>
<li><a href="https://github.com/web-platform-tests/interop/issues/563">requestIdleCallback</a></li>
<li><a href="https://github.com/web-platform-tests/interop/issues/501">Declarative Shadow DOM</a></li>
<li><a href="https://github.com/web-platform-tests/interop/issues/521">attr() support extended capabilities</a></li>
<li><a href="https://github.com/web-platform-tests/interop/issues/464">Web Share API</a></li>
<li><a href="https://github.com/web-platform-tests/interop/issues/553">details and summary elements</a></li>
<li><a href="https://github.com/web-platform-tests/interop/issues/442">CSS element() function</a></li>
<li><a href="https://github.com/web-platform-tests/interop/issues/571">scrollbar-width</a>, <a href="https://github.com/web-platform-tests/interop/issues/417">scrollbar-color</a>, <a href="https://github.com/web-platform-tests/interop/issues/419">scrollbar-gutter</a></li>
<li><a href="https://github.com/web-platform-tests/interop/issues/541">font-size-adjust</a>, <a href="https://github.com/web-platform-tests/interop/issues/542">size-adjust</a></li>
</ul>
<h2>Notable items low on my list</h2>
<p>The point of the &quot;game&quot; is prioritisation, and I thought it would be interesting to look at what proposals are popular that I don't prioritise. Clearly lots of people want them so it's definitely not a case of them not bing valuable, just not relevant to me for whatever reason!</p>
<ul>
<li><a href="https://github.com/web-platform-tests/interop/issues/423">Popover</a> — honestly, I've never had difficulty implementing &quot;popovers&quot; and I'm not sure I love the API.</li>
<li><a href="https://github.com/web-platform-tests/interop/issues/522">WebXR</a> — Nothing on the API, I've just gone off VR and never really 'got' AR.</li>
<li><a href="https://github.com/web-platform-tests/interop/issues/421">Custom Media Queries</a> — I actually prefer the API of the style queries 'workaround'.</li>
<li><a href="https://github.com/web-platform-tests/interop/issues/573">Allowing &lt;hr&gt; inside of &lt;select&gt;</a> — Kinda just a meh version of <code>optgroup</code>? See <a href="https://adrianroselli.com/2023/10/splitting-within-selects.html">Adrian Roselli's Splitting within Selects</a></li>
</ul>

      <hr>
      <p>If you have any comments or feedback on this article, let me know! I'd love to hear your thoughts, go ahead and <a href="mailto:alistair@accudio.com">send me an email</a> at alistair@accudio.com or <a href="https://front-end.social/@accudio" rel="noopener noreferrer">contact me on Mastodon</a>.</p>
      <p>The post <a href="https://www.alistairshepherd.uk/writing/interop-priority-game/">Interop Priority Game 2024</a> appeared first on <a href="https://alistairshepherd.uk">alistairshepherd.uk</a>.</p>
    ]]>
      </content>
    </entry>
  
    
    <entry>
      <title>HTMHell Advent Calendar — Getting started with Web Performance</title>
      <link href="https://www.alistairshepherd.uk/writing/htmhell-advent-calendar-23/"/>
      <updated>2023-12-14T00:00:00Z</updated>
      <id>https://www.alistairshepherd.uk/writing/htmhell-advent-calendar-23/</id>
      <content type="html">
        <![CDATA[
      <p>A post on the the HTMHell Advent Calendar about how to get started with web performance including some tips!</p>
<p><a href="https://www.htmhell.dev/adventcalendar/2023/14/">Available in full on the HTMHell Advent Calendar</a></p>

      <hr>
      <p>If you have any comments or feedback on this article, let me know! I'd love to hear your thoughts, go ahead and <a href="mailto:alistair@accudio.com">send me an email</a> at alistair@accudio.com or <a href="https://front-end.social/@accudio" rel="noopener noreferrer">contact me on Mastodon</a>.</p>
      <p>The post <a href="https://www.alistairshepherd.uk/writing/htmhell-advent-calendar-23/">HTMHell Advent Calendar — Getting started with Web Performance</a> appeared first on <a href="https://alistairshepherd.uk">alistairshepherd.uk</a>.</p>
    ]]>
      </content>
    </entry>
  
    
    <entry>
      <title>Web Performance Calendar — Ten optimisation tips for an initial web performance audit</title>
      <link href="https://www.alistairshepherd.uk/writing/web-performance-calendar-23/"/>
      <updated>2023-12-14T00:00:00Z</updated>
      <id>https://www.alistairshepherd.uk/writing/web-performance-calendar-23/</id>
      <content type="html">
        <![CDATA[
      <p>A post on the the Web Performance Calendar with handy tips for an site performance audit</p>
<p><a href="https://calendar.perfplanet.com/2023/ten-optimisation-tips-for-an-initial-web-performance-audit/">Available in full on the Web Performance Calendar</a></p>

      <hr>
      <p>If you have any comments or feedback on this article, let me know! I'd love to hear your thoughts, go ahead and <a href="mailto:alistair@accudio.com">send me an email</a> at alistair@accudio.com or <a href="https://front-end.social/@accudio" rel="noopener noreferrer">contact me on Mastodon</a>.</p>
      <p>The post <a href="https://www.alistairshepherd.uk/writing/web-performance-calendar-23/">Web Performance Calendar — Ten optimisation tips for an initial web performance audit</a> appeared first on <a href="https://alistairshepherd.uk">alistairshepherd.uk</a>.</p>
    ]]>
      </content>
    </entry>
  
    
    <entry>
      <title>My 2023 round-up</title>
      <link href="https://www.alistairshepherd.uk/writing/2023-roundup/"/>
      <updated>2024-01-24T00:00:00Z</updated>
      <id>https://www.alistairshepherd.uk/writing/2023-roundup/</id>
      <content type="html">
        <![CDATA[
      <p>Happy very belated new year! For the most part I had a really good 2023, I hope you did also. This is a month or so later than I meant to, as I had a pretty rough late December and very busy January dealing with the fallout. Enough of my complaining though, what was my year like? Well...</p>
<h2>Work</h2>
<p>I started a new job at the beginning of 2023, taking on the position of Lead Developer at <a href="https://serieseight.com">Series Eight</a>. It's been a really interesting and challenging year, going from being mostly focused on code to instead people and processes. Although I haven't yet achieved everything I set out to, I'm happy with the work I've done and the impact I've had. There's still a lot for me to work on, particularly my stress levels! This year I'm making my goal to be more relaxed and rely on the team more.</p>
<p>Some of the this I'm proud of myself for at work:</p>
<ul>
<li>A <strong>major</strong> focus on accessibility, training the team from little accessibility knowledge to consistently close to WCAG AA conformance;</li>
<li>Improving consistency in builds with starter projects;</li>
<li>Overhauling our hosting setup for much better performance with a CDN;</li>
<li>Being involved in some pretty cool and tricky websites including <a href="https://smilemakerscollection.com">Smile Makers</a>, <a href="https://www.prudenandsmith.com">Pruden &amp; Smith</a> <a href="https://www.thewoodveneerhub.com.au">The Wood Veneer Hub</a> and <a href="https://www.viatu.com/en">Viatu</a></li>
</ul>
<h2>Conferences</h2>
<p>I spoke at one conference this year and attended a couple further. I intended to speak at and attend more this year, but focusing on my new job and life got in the way of that.</p>
<p>If you're involved in a conference or meetup for 2024 then please get in touch! I'm updating my current conference talk &quot;Creative Web: Building dynamic websites for work and play&quot; with some exciting new CSS functionality, and also working on a web performance talk called &quot;Making websites fly with web performance for all&quot; for this and next year.</p>
<ul>
<li>In early May I was once again attending All Day Hey! which was fantastic as always. Alongside a couple colleagues I <a href="https://serieseight.com/journal/hey-all-day-23">wrote about my thoughts on the Series Eight blog</a>.</li>
<li>State of the Browser in September is one of the highlights of my year. The talks are always top quality, the event is fantastically organised (❤️ Dave), and I see so many friends each time! As with Hey, I <a href="https://serieseight.com/journal/sotb23">wrote a little about each talk on the S8 blog</a>.</li>
<li>In November <a href="https://www.youtube.com/watch?v=jBcjQHO4Gr4">I spoke at Halfstack Conf London</a>, giving my Creative Web talk. It went really well and I'm really happy with how smooth and natural it felt, definitely the best I've felt speaking. It was a different crowd from what I'm used to and really nice to meet lots of new people.</li>
</ul>
<h2>Side projects</h2>
<p><a href="https://async-alpine.dev">Async Alpine</a> has done well this year and really taken off into something of it's own! There's been a couple of small releases, including contributions from others, inclusion as a dependency for bigger projects, and had almost 20,000 downloads from npm over 2023.</p>
<p>I experimented a bit with &quot;AI&quot; earlier this year and made <a href="https://wackyhoroscopes.accudio.com">Wacky Horoscopes</a>, a site that gives silly daily horoscopes that were generated by an LLM. I'm not entirely comfortable with a project using an LLM to be honest, as is extremely obvious from my writing on the &quot;About&quot; page! I really like the concept, find the result funny, and am extremely happy with the frontend and 11ty build, but in hindsight my current stance on LLMs makes me regret it. That said, the horoscopes have been generated now and there's no further LLM involved so it would be a shame to take it down I think? I'm still not sure.</p>
<p>Near the end of the year I wrote <a href="https://github.com/Accudio/ridge-map">Ridge Map</a>, a node library to generate cool visualisations of elevation data in SVG. I thought it would take a couple hours and it ended up taking me 4 days — it was so much harder than I expected! Check out my &quot;Arts and Crafts&quot; section below for more photos of the results.</p>
<img class="measure-long rounded" src="/images/ridge-map-everest.png?width=1077" srcset="/images/ridge-map-everest.png?width=400 400w, /images/ridge-map-everest.png?width=600 600w, /images/ridge-map-everest.png?width=800 800w, /images/ridge-map-everest.png?width=1077 1077w, /images/ridge-map-everest.png?width=1500 1500w, /images/ridge-map-everest.png?width=2154 2154w" sizes="(min-width: 70rem) 67.34rem, calc(100vw - 2.66rem)" alt="Ridge map of the Himalayas, centred around Mt Everest" width="1000" height="380" loading="lazy">
<h2>Writing</h2>
<p>2023 was a huge year for writing for me, with two articles published somewhere that wasn't my blog (or my work's), and went through an editor who really knew their stuff!</p>
<p><a href="https://www.htmhell.dev/adventcalendar/2023/14/">Getting started with Web Performance 🚀</a> was published by Manuel Matuzovic in the HTMHell Advent Calendar and I am thrilled with it. My goal was something that any web developer of any level can use to familiarise themselves with the motive, concepts, jargon, and tools of web performance, and also include some suggestions of things to check first to get started. I feel like I managed to deliver that pretty well, the reception was great, and I'm also working on turning it into a conference talk. A <strong>huge</strong> thank you to Manuel and the other reviewers for helping me out with it.</p>
<p>I also wrote <a href="https://calendar.perfplanet.com/2023/ten-optimisation-tips-for-an-initial-web-performance-audit/">Ten optimisation tips for an initial web performance audit</a> for the Web Performance Calendar which slightly blew my mind to be honest. I've followed the calendar for a while and I still can't really believe I am of the standard to have an article published there! This and the HTMHell articles are based on the same initial plan but focused on different audiences. If you want to go in-depth into those suggestions this article has more research, details and references.</p>
<p>I've also written quite a bit this year on my own blog:</p>
<ul>
<li><a href="https://alistairshepherd.uk/writing/grapheneos/">GrapheneOS as my daily-driver mobile OS</a></li>
<li><a href="https://alistairshepherd.uk/writing/hillwalking-maps-with-leaflet/">Displaying Hillwalking routes on the web with GPX files and Leaflet</a></li>
<li><a href="https://alistairshepherd.uk/writing/mastodon-android/">Searching for a Mastodon app for Android</a></li>
<li><a href="https://alistairshepherd.uk/writing/vercel-geoip/">Simple, cheap GeoIP API using Vercel Edge functions</a></li>
<li><a href="https://alistairshepherd.uk/writing/netlify-geoip/">Simple, cheap GeoIP API using Netlify Edge functions</a></li>
<li><a href="https://alistairshepherd.uk/writing/aeropex-review/">Bone conduction earphones revolution — Aftershokz Aeropex review</a></li>
<li><a href="https://alistairshepherd.uk/writing/im-a-luddite-alt-text/">Alternative text for &quot;I'm a Luddite (and So Can You!)&quot;</a></li>
<li><a href="https://alistairshepherd.uk/writing/interop-priority-game/">Interop Priority Game 2024</a></li>
</ul>
<p>And a handful of posts on the Series Eight blog:</p>
<ul>
<li><a href="https://serieseight.com/journal/hey-all-day-23">Our biggest takeaways from Hey! All Day 2023</a></li>
<li><a href="https://serieseight.com/journal/shopify-winter-23">Shopify’s Winter ‘23 release in action</a></li>
<li><a href="https://serieseight.com/journal/shopify-summer-23">Shopify Summer ‘23 Editions: first impressions and best additions</a></li>
<li><a href="https://serieseight.com/journal/sotb23">What we learned at State of the Browser 2023</a></li>
</ul>
<h2>Travel and holidays</h2>
<p>I had a few holidays this year, mostly pretty domestic. I went on a group holiday with university friends to the Peak District, England in June, and to Bridlington, England in December (which has a wonderful zoo with ducks in it turns out).</p>
<div class="auto-grid" style="--auto-grid-fit:auto-fit;--auto-grid-size:">
  <img class="rounded" src="/images/lea-hall-me.jpg?width=528" srcset="/images/lea-hall-me.jpg?width=400 400w, /images/lea-hall-me.jpg?width=600 600w, /images/lea-hall-me.jpg?width=800 800w, /images/lea-hall-me.jpg?width=1077 1077w, /images/lea-hall-me.jpg?width=1500 1500w, /images/lea-hall-me.jpg?width=2154 2154w" sizes="100vw, (min-width: 37rem) 50vw, (min-width: 70rem) 33rem" alt="Me wearing a pride flag kilt, black shirt and smiling holding a champagne glass" width="3212" height="4283" loading="lazy">
  <img class="rounded" src="/images/bridlington-ducks.jpg?width=528" srcset="/images/bridlington-ducks.jpg?width=400 400w, /images/bridlington-ducks.jpg?width=600 600w, /images/bridlington-ducks.jpg?width=800 800w, /images/bridlington-ducks.jpg?width=1077 1077w, /images/bridlington-ducks.jpg?width=1500 1500w, /images/bridlington-ducks.jpg?width=2154 2154w" sizes="100vw, (min-width: 37rem) 50vw, (min-width: 70rem) 33rem" alt="A group of ducks hanging out in a muddy enclosure" width="1604" height="2139" loading="lazy">
</div>
<p>My dad moved from the Isle of Skye down to south Wales so I went for a final easy visit to Skye and visited his new house in Wales. I've only been to north Wales a couple of times so I'm keen to explore it a bit more! When we visited was a heatwave so the temperature, lovely golden beaches, surfing and non-English street signs made it feel like the Mediterranean!</p>
<p>In October I went to Prague with work for a short get-together and socialise. It's a beautiful city and it was fantastic to meet up with the team in person again, even if I don't yet forgive my boss Mario for the amount of Absinthe I drank on the final night.</p>
<h2>Arts and crafts</h2>
<p>I'll remember this year for the fact that after about 5 years my design burnout has faded enough for me to finally enjoy designing again! In the summer I started to get into sticker making, and ended up buying myself a Silhouette Cameo 4 cutting machine to more easily produce stickers. I've designed and printed a handful of stickers related to Scotland, development and silly stickers for friends.</p>
<p>It's been good fun to experiment with and make things!</p>
<img class="measure-long rounded" src="/images/stickers.jpg?width=1077" srcset="/images/stickers.jpg?width=400 400w, /images/stickers.jpg?width=600 600w, /images/stickers.jpg?width=800 800w, /images/stickers.jpg?width=1077 1077w, /images/stickers.jpg?width=1500 1500w, /images/stickers.jpg?width=2154 2154w" sizes="(min-width: 70rem) 67.34rem, calc(100vw - 2.66rem)" alt="Bunch of assorted stickers including development, scottish destinations and silly stickers on a black background" width="2000" height="1417" loading="lazy">
<p>What has really inspired me however is using the cutting machine instead as a pen plotter. You buy an adaptor and some nice pens, and you can have the Cameo draw out the SVG you've designed onto paper. This is right up my alley because it makes art more accessible to me — I can design and draw digital art using code or a graphics program and have the machine put it right onto paper at really nice quality!</p>
<p>Contour prints were where I got started with this but I then wrote <a href="https://github.com/Accudio/ridge-map">Ridge Map</a> in order to generate 3D elevation visualisations to plot. They were my go-to Christmas gift this year, and I even did some of Mars landmarks with watercolour highlights.</p>
<div class="auto-grid" style="--auto-grid-fit:auto-fit;--auto-grid-min-size:22rem">
  <img class="rounded" src="/images/plot-contours.jpg?width=528" srcset="/images/plot-contours.jpg?width=400 400w, /images/plot-contours.jpg?width=600 600w, /images/plot-contours.jpg?width=800 800w, /images/plot-contours.jpg?width=1077 1077w, /images/plot-contours.jpg?width=1500 1500w, /images/plot-contours.jpg?width=2154 2154w" sizes="100vw, (min-width: 37rem) 50vw, (min-width: 70rem) 33rem" alt="Pen plot in dark blue in a black frame of the contours of the Cairngorm Mountains" width="2000" height="1400" loading="lazy">
  <img class="rounded" src="/images/plot-ridge.jpg?width=528" srcset="/images/plot-ridge.jpg?width=400 400w, /images/plot-ridge.jpg?width=600 600w, /images/plot-ridge.jpg?width=800 800w, /images/plot-ridge.jpg?width=1077 1077w, /images/plot-ridge.jpg?width=1500 1500w, /images/plot-ridge.jpg?width=2154 2154w" sizes="100vw, (min-width: 37rem) 50vw, (min-width: 70rem) 33rem" alt="Pen plot in dark blue in a black frame illustrating the ridges of Cairngorm Mountain from the north" width="2000" height="1400" loading="lazy">
  <img class="rounded" src="/images/plot-mars.jpg?width=528" srcset="/images/plot-mars.jpg?width=400 400w, /images/plot-mars.jpg?width=600 600w, /images/plot-mars.jpg?width=800 800w, /images/plot-mars.jpg?width=1077 1077w, /images/plot-mars.jpg?width=1500 1500w, /images/plot-mars.jpg?width=2154 2154w" sizes="100vw, (min-width: 37rem) 50vw, (min-width: 70rem) 33rem" alt="Four postcards showing ridge maps of Mars landmarks with coloured watercolour backgrounds. The landmarks are Olympus Mons, Valles Marineris, Gale Crater and Jezero Crater" width="2000" height="1400" loading="lazy">
  <img class="rounded" src="/images/plot-waves.jpg?width=528" srcset="/images/plot-waves.jpg?width=400 400w, /images/plot-waves.jpg?width=600 600w, /images/plot-waves.jpg?width=800 800w, /images/plot-waves.jpg?width=1077 1077w, /images/plot-waves.jpg?width=1500 1500w, /images/plot-waves.jpg?width=2154 2154w" sizes="100vw, (min-width: 37rem) 50vw, (min-width: 70rem) 33rem" alt="Postcard with light blue watercolour background and dark blue waves in a line style" width="2000" height="1400" loading="lazy">
</div>
<p>I'm keen to keep making things! I'm not sure how 'professionally' I'll do it, on one hand it could be nice to sell art to people who like it online or at local design stores, on the other hand I don't want yet another full-on job! If you do want any of my stickers, prints or want a commission however let me know and I'd love to do so for internet pals!</p>
<h2>Media</h2>
<p>I didn't play, watch, read or listen to as much this year, instead focusing on blog posts, side projects and arts &amp; crafts. Because of that I'll do short paragraphs about the media I don't have many things to note for, but proper sections for Games and Music.</p>
<p>Getting movies and TV out the way, the only release worth mentioning for me is Barbie — obviously I adored it! I did also go see Rocky Horror Picture Show for a late screening on the day of Edinburgh Pride with free wine and I tell you what that was <strong>amazing</strong>.</p>
<p>This year I saw more at the Edinburgh fringe than normal, my favourite show was <a href="https://tickets.edfringe.com/whats-on/tragedy-that-befalls-the-dastardly-crew-of-the-kakapo-1">The Tragedy That Befalls the Dastardly Crew of the Kakapo</a>, a perfect example of a Fringe farce with no budget that was side-splittingly hilarious. On live theatre, I also saw <a href="https://pitlochryfestivaltheatre.com/whats-on/sunshine-on-leith-2023/">Sunshine on Leith at Pitlochry Theatre</a> in November which was fantastic and made me feel extremely patriotic of Scotland, Edinburgh and Leith.</p>
<p>I don't read a lot, but if you're interested in my reading then check out <a href="https://literal.club/accudio">my Literal account</a> (similar to Goodreads). Highlights are <a href="https://literal.club/accudio/book/dan-schreiber-the-theory-of-everything-else-547e6">The Theory of Everything Else by Dan Schreiber</a>, and <a href="https://literal.club/book/sistersong-pqi19">Sistersong by Lucy Holland</a>.</p>
<p>On podcasts there was nothing new for me, more <a href="http://restishistorypod.com/">Rest is History</a>, <a href="https://www.nosuchthingasafish.com">No Such Thing as a Fish</a>, <a href="https://f-word.dev">The F-Word</a> and <a href="https://vanillajspodcast.com">The Vanilla JS Podcast</a>.</p>
<h3>Games</h3>
<p>I didn't play much but there were a few games worth noting! This list is specifically the games I started in 2023, you can <a href="/games/">instead check out my favourite games ever</a>.</p>
<ul>
<li><a href="https://store.steampowered.com/app/2377000/Golden_Idol_Mysteries_The_Spider_of_Lanka/">The Case of the Golden Idol DLC: The Spider of Lanka</a> is a fantastic DLC to one of my favourite games of all time. It's a genuinely tricky detective mystery set in a weird and beautiful pixel world. It's similar to the original in feel, but unique enough to pull it off.</li>
<li><a href="https://www.yonderchronicles.com">Yonder: The Cloud Catcher Chronicles</a> is a relaxing open-world game where you explore, Craft, do quests and farm. Really nice chill gaming that I really enjoyed! Also one of the collectibles is cute cats.</li>
<li>In <a href="https://www.terranil.com/">Terra Nil</a> you use machines to restore nature to a dead world and then clean up after yourself and leave. A short but lovely strategy builder game I'd highly recommend.</li>
<li><a href="https://store.steampowered.com/app/986130/Shadows_of_Doubt/">Shadows of Doubt</a> is a randomly-generated open-world detective game that is really cool. Murders happen in a dystopian sci-fi voxel-based city and you have to work out who dunnit. I really enjoyed it but found it pretty buggy so will be coming back to it in a year or two when they've polished it a little more.</li>
<li><a href="https://borderlands.2k.com">Borderlands 3</a> is pretty fun action shooter with silly mechanics and writing. It's not the best in the world. but good fun, particularly with friends.</li>
</ul>
<h3>Music</h3>
<p>Due to the increased number of calls I'm in at work now, the amount of music I listen to has decreased a fair bit but it's still a lot. Working from home I guess! I listened to a lot of artists and albums from the past few years rather than 2023 releases, so these are artists and albums that I particularly enjoyed in 2023.</p>
<ul>
<li><a href="https://album.link/i/1611606244">MUNA's self-titled album</a> came out in 2022 but I <strong>really</strong> got into it this year. That album was 5 of my top ten songs this year. I love <em>Silk Chiffon</em> and <em>Kind of Girl</em>.</li>
<li>Until this year Portobello had only five songs and one was my favourite ever. This year they released two new singles, <a href="https://song.link/gb/i/1697986349"><em>Dance (In the City)</em></a> and <a href="https://song.link/gb/i/1697986867"><em>Lose My Mind</em></a> which are both bops I played the shit out of.</li>
<li>I got into Claud this year, particularly enjoying their 2021 <a href="https://album.link/gb/i/1539093038">Super Monster</a> but also this year's <a href="https://album.link/gb/i/1678606174">Supermodels</a>.</li>
<li>Wild Party has a nice new album with <a href="https://album.link/gb/i/1697074476">Get Up</a> satisfied my confusing but undeniable taste for very indie bands from southern US that no one has heard of.</li>
</ul>

      <hr>
      <p>If you have any comments or feedback on this article, let me know! I'd love to hear your thoughts, go ahead and <a href="mailto:alistair@accudio.com">send me an email</a> at alistair@accudio.com or <a href="https://front-end.social/@accudio" rel="noopener noreferrer">contact me on Mastodon</a>.</p>
      <p>The post <a href="https://www.alistairshepherd.uk/writing/2023-roundup/">My 2023 round-up</a> appeared first on <a href="https://alistairshepherd.uk">alistairshepherd.uk</a>.</p>
    ]]>
      </content>
    </entry>
  
    
    <entry>
      <title>Simple, fast build tooling with live reload for a non-framework website</title>
      <link href="https://www.alistairshepherd.uk/writing/simple-build-tooling/"/>
      <updated>2024-07-14T00:00:00Z</updated>
      <id>https://www.alistairshepherd.uk/writing/simple-build-tooling/</id>
      <content type="html">
        <![CDATA[
      <p>I found a nice build tooling setup that can be integrated into non-framework websites where some part isn't controlled by the build tool. WordPress, Craft, Kirby or other CMS' are a good example. My setup uses Parcel, livereload and only a few node scripts to work. <a href="#setup">Jump to the details</a> or <a href="#context">stick around for the context</a>.</p>
<h2 id="context" tabindex="-1">Context</h2>
<p>Yesterday I started a new project for a family member, a simple WordPress website built with a custom from-scratch theme. One of the first thing to sort out is build tooling to bundle my CSS and JavaScript.</p>
<p>Most build tools now seem to be geared around the 'in fashion' technologies like React, Vue etc. I come back to build tooling relatively often, wondering if the tech world has understood yet that making 'index.html' a requirement for your build tool to work makes it useless to a huge number of developers.</p>
<p>If you weren't aware, many build tools want to own your entire website build including HTML and all assets. That works great when your whole website runs through your build, but for dynamic sites that have their own asset loading systems — like WordPress and countless other CMS' — that just doesn't work. Whilst many build tools do offer an alternative it is often second-rate, complex or doesn't have the same functionality I expect from a modern development setup.</p>
<p>That leads me onto my day yesterday. I wanted to work out finally how I can get a good modern development setup on this WordPress site. My requirements:</p>
<ul>
<li>Bundles my Scss, JS, inlines small images, imports from npm with tree-shaking. Standard build stuff.</li>
<li>Uses versioned/hashed asset names to make caching easier and gives me those names in a way I can use in PHP.</li>
<li>Fast. I don't want to wait for builds.</li>
<li>Instant hot switching of CSS without need for a reload.</li>
<li>Automatic reloading of my browser when JS, HTML or PHP changes.</li>
</ul>
<p>Turns out this was harder to achieve than I expected. Let's go through the candidates:</p>
<p><a href="https://laravel-mix.com">Laravel Mix</a> is what I'm used to from work and is what a lot of the PHP community uses. Simple, declarative way of bundling assets based on Webpack. It does have Browsersync out-of-the-box for automatic reloading, but it need to proxy your site to do that and I've found it a bit inconsistent. When using Mix I generally manually reload as it's more reliable.</p>
<p><a href="https://webpack.js.org">Webpack</a> is awful and I hate it. The configuration system is terrible and indecipherable and in my opinion it is always the wrong decision for a build tool.</p>
<p><a href="https://vitejs.dev">Vite</a> is the hot new kid on the block. It's very much designed for frameworks or for if it's also handling your index.html. It is possible to make it work with WordPress etc, but I haven't seen a setup that looks great and their documentation is pretty poor if you opt for that route.</p>
<p><a href="https://rollupjs.org">RollUp</a> looks good and I'd like to try it, but I find it quite difficult to actually get what I want from it. I've played with it a few times and just couldn't find a setup that was as flexible as I wanted. That may just be me not understanding right but that I can't work out how to get a simple CSS/JS bundler set up is not a good sign.</p>
<p>A custom system with node scripts and importing packages? It would probably work but there is a lot to deal with that build tools have already been through. Inlining a 600byte SVG into my CSS file is something I want but don't want to have to build myself.</p>
<p><a href="https://parceljs.org">Parcel</a> was what I ended up settling on. It's a little like Vite where it's built for the simple, no config, index.html setup, but I find it's a lot more versatile and easy to use in other circumstances, and it's docs are a lot better for non-standard usecases.</p>
<h2 id="setup" tabindex="-1">The set up</h2>
<h3>Step 0. Installation</h3>
<p>If you're following along with implementing this yourself, you'll need:</p>
<pre class="language-sh"><code class="language-sh"><span class="token function">npm</span> <span class="token function">install</span> <span class="token parameter variable">-D</span> parcel livereload pug</code></pre>
<p>For the commands below you'll be able to use them from npm scripts no problems, but if you're running on the command line yourself add <code>npx </code> in front of each.</p>
<h3>Step 1. Asset bundling</h3>
<p>Parcel handles compilation of most stuff pretty easily and out of the box using <code>link</code> and <code>script</code> tags, so I'm starting there:</p>
<pre class="language-html"><code class="language-html"><span class="token comment">&lt;!-- src/assets.html --></span><br><span class="token tag"><span class="token tag"><span class="token punctuation">&lt;</span>link</span> <span class="token attr-name">href</span><span class="token attr-value"><span class="token punctuation attr-equals">=</span><span class="token punctuation">"</span>./css/main.scss<span class="token punctuation">"</span></span> <span class="token attr-name">rel</span><span class="token attr-value"><span class="token punctuation attr-equals">=</span><span class="token punctuation">"</span>stylesheet<span class="token punctuation">"</span></span> <span class="token punctuation">/></span></span><br><span class="token tag"><span class="token tag"><span class="token punctuation">&lt;</span>script</span> <span class="token attr-name">type</span><span class="token attr-value"><span class="token punctuation attr-equals">=</span><span class="token punctuation">"</span>module<span class="token punctuation">"</span></span> <span class="token attr-name">src</span><span class="token attr-value"><span class="token punctuation attr-equals">=</span><span class="token punctuation">"</span>./js/main.js<span class="token punctuation">"</span></span><span class="token punctuation">></span></span><span class="token script"></span><span class="token tag"><span class="token tag"><span class="token punctuation">&lt;/</span>script</span><span class="token punctuation">></span></span></code></pre>
<p>You can run the production build with <code>parcel build ./src/assets.html --public-url /dist/</code>. This will bundle and import my <code>main.scss</code> and <code>main.js</code> files, and output <code>dist/assets.html</code> that includes the HTML to those versioned assets, which looks like this:</p>
<pre class="language-html"><code class="language-html"><span class="token tag"><span class="token tag"><span class="token punctuation">&lt;</span>link</span> <span class="token attr-name">href</span><span class="token attr-value"><span class="token punctuation attr-equals">=</span><span class="token punctuation">"</span>/dist/assets.50a966ac.css<span class="token punctuation">"</span></span> <span class="token attr-name">rel</span><span class="token attr-value"><span class="token punctuation attr-equals">=</span><span class="token punctuation">"</span>stylesheet<span class="token punctuation">"</span></span><span class="token punctuation">></span></span><br><span class="token tag"><span class="token tag"><span class="token punctuation">&lt;</span>script</span> <span class="token attr-name">src</span><span class="token attr-value"><span class="token punctuation attr-equals">=</span><span class="token punctuation">"</span>/dist/assets.fd9c92df.js<span class="token punctuation">"</span></span> <span class="token attr-name">defer</span><span class="token attr-value"><span class="token punctuation attr-equals">=</span><span class="token punctuation">"</span><span class="token punctuation">"</span></span><span class="token punctuation">></span></span><span class="token script"></span><span class="token tag"><span class="token tag"><span class="token punctuation">&lt;/</span>script</span><span class="token punctuation">></span></span></code></pre>
<p>I can import <code>/dist/assets.html</code> into the output of my page — for example using PHPs <code>file_get_contents</code>. We can change the path used in the output URLs if needed with the <code>--public-url</code> parameter — for my WordPress theme I use <code>--public-url /wp-content/themes/theme/dist</code>.</p>
<p>Now we have asset bundling with URL hashing that we can import into our templates. The build is also really fast. Nice!</p>
<h3>Step 2. Parcel reloading and dev server</h3>
<p>Okay so we've got a build, but it's one-off and doesn't do anything about reloading. Next step is setting Parcel up to reload. Although most guides will push you down Parcel's serve mode to serve your assets here, the way to go is actually using watch mode, with this script:</p>
<pre class="language-sh"><code class="language-sh">parcel <span class="token function">watch</span> ./src/assets.html --public-url /dist/<br>  --hmr-host localhost --hmr-port <span class="token number">1234</span></code></pre>
<p>This will run a watcher that compiles the assets and html file whenever CSS or JS files change. It also runs an HMR (Hot Module Reloading) server on <code>localhost:1234</code> and injects some code into <code>main.js</code> that will check if any assets or the page needs reloaded.</p>
<p>As we're importing <code>assets.html</code> into our site which Parcel is auto-generating, this will pick up the changes between build and dev mode with no further changes. Run the dev parcel command, refresh the page and now we have hot CSS replacement and reloading on JS changes.</p>
<h3>Step 3. Livereload for HTML and PHP changes</h3>
<p>Whilst Parcel is handling the reloads of CSS and JS nicely, it doesn't do any updates when the HTML or PHP changes. I spent a while going down a rabbit hole of trying to work out if I could piggyback on Parcel's HMR system but found no way of doing that so we're using <a href="https://www.npmjs.com/package/livereload">livereload</a> instead.</p>
<p>Run livereload with this command, which watches HTML and PHP files for changes and triggers a reload. I'm also excluding the dist directory as that would result in a double refresh when Parcel builds <code>assets.html</code>. You can add other extensions here like your templating language.</p>
<pre class="language-sh"><code class="language-sh">livereload <span class="token parameter variable">-e</span> <span class="token string">'html,php'</span> <span class="token parameter variable">-x</span> dist/ <span class="token parameter variable">-p</span> <span class="token number">1235</span></code></pre>
<p>You'll also need to add the below script to your page to communicate with livereload:</p>
<pre class="language-html"><code class="language-html"><span class="token tag"><span class="token tag"><span class="token punctuation">&lt;</span>script</span> <span class="token attr-name">src</span><span class="token attr-value"><span class="token punctuation attr-equals">=</span><span class="token punctuation">"</span>http://localhost:1235/livereload.js?snipver=1<span class="token punctuation">"</span></span><span class="token punctuation">></span></span><span class="token script"></span><span class="token tag"><span class="token tag"><span class="token punctuation">&lt;/</span>script</span><span class="token punctuation">></span></span></code></pre>
<p>When we have both Parcel and livereload running we have a hot replacement of CSS and a page reload on JS, HTML and PHP changes! All very fast and works pretty well. One final thing...</p>
<h3>Step 4. Automatic addition of livereload script</h3>
<p>One problem with what we have now is we need a way to include the livereload script when it's needed in development, but not in the live site. This seemed tricky at first, perhaps we change an environment variable or something we can pick up in the site code? Turns out there's an easier way!</p>
<p>We're likely going to want to run Parcel and Livereload together during development, and Parcel is already building the <code>assets.html</code> differently depending on dev/production. So we can use Parcel to inject the livereload script only in development.</p>
<p>Parcel can't use functional logic in HTML files, but it can use it in files that build to HTML, most notably Pug! Pug is a templating language that compiles to HTML and includes stuff like if statements etc. So we can rename <code>src/assets.html</code> to <code>src/assets.pug</code> and add to our asset markup:</p>
<pre class="language-pug"><code class="language-pug"><span class="token markup"><span class="token tag"><span class="token tag"><span class="token punctuation">&lt;</span>link</span> <span class="token attr-name">href</span><span class="token attr-value"><span class="token punctuation attr-equals">=</span><span class="token punctuation">"</span>./css/main.scss<span class="token punctuation">"</span></span> <span class="token attr-name">rel</span><span class="token attr-value"><span class="token punctuation attr-equals">=</span><span class="token punctuation">"</span>stylesheet<span class="token punctuation">"</span></span> <span class="token punctuation">/></span></span></span><br><span class="token markup"><span class="token tag"><span class="token tag"><span class="token punctuation">&lt;</span>script</span> <span class="token attr-name">type</span><span class="token attr-value"><span class="token punctuation attr-equals">=</span><span class="token punctuation">"</span>module<span class="token punctuation">"</span></span> <span class="token attr-name">src</span><span class="token attr-value"><span class="token punctuation attr-equals">=</span><span class="token punctuation">"</span>./js/main.js<span class="token punctuation">"</span></span><span class="token punctuation">></span></span><span class="token script"></span><span class="token tag"><span class="token tag"><span class="token punctuation">&lt;/</span>script</span><span class="token punctuation">></span></span></span><br><span class="token flow-control"><span class="token branch keyword">if</span> process<span class="token punctuation">.</span>env<span class="token punctuation">.</span><span class="token constant">NODE_ENV</span> <span class="token operator">!==</span> <span class="token string">'production'</span></span><br>	<span class="token markup"><span class="token tag"><span class="token tag"><span class="token punctuation">&lt;</span>script</span> <span class="token attr-name">src</span><span class="token attr-value"><span class="token punctuation attr-equals">=</span><span class="token punctuation">"</span>http://localhost:1235/livereload.js?snipver=1<span class="token punctuation">"</span></span><span class="token punctuation">></span></span><span class="token script"></span><span class="token tag"><span class="token tag"><span class="token punctuation">&lt;/</span>script</span><span class="token punctuation">></span></span></span></code></pre>
<p>This will only include the livereload snippet if the <code>NODE_ENV</code> environment variable is not <code>production</code>, which means it won't be included in our production build when we run <code>parcel build ...</code>.</p>
<h2>Final thoughts and the complete code</h2>
<p>I'm really happy with this dev setup! It ticks all of my boxes from earlier, it's relatively simple and flexible, and brings a really nice modern dev setup to sites like WordPress or other CMS builds.</p>
<p>There are definitely some limitations, beyond the standard ones when you don't process HTML with your build tool. The main trickiness I've found is that whilst CSS and JS in the <code>head</code> works great it's a bit faffy to have more structure. You can generate more html files with alternative assets so you could have <code>head.html</code>, <code>footer.html</code>, <code>about-page.html</code>. That does get a little messy and verbose however, and image processing within the build would also require the same treatment.</p>
<p>However, I don't believe I've seen a good development setup with this kind of website that handles this better. A purely up-front build tool like Laravel Mix has the same problems, and none of the 'fancier' tools seem to have a better solution either.</p>
<p>So yeah, I think this is what I'll be using for my own sites going forward, certainly with CMS', I'll see how it compares against my other methods for 11ty sites in future!</p>
<p>If you're just looking to copy+paste, run the <code>npm install</code> command and copy the <code>assets.pug</code> from above. This is my <code>package.json</code> scripts, with <code>npm-run-all</code> for running both Parcel and livereload at the same time:</p>
<pre class="language-json"><code class="language-json"><span class="token punctuation">{</span><br>  <span class="token property">"scripts"</span><span class="token operator">:</span> <span class="token punctuation">{</span><br>    <span class="token property">"dev"</span><span class="token operator">:</span> <span class="token string">"run-p dev:*"</span><span class="token punctuation">,</span><br>    <span class="token property">"dev:parcel"</span><span class="token operator">:</span> <span class="token string">"parcel watch ./src/assets.pug --public-url /dist/ --hmr-host localhost --hmr-port 1234"</span><span class="token punctuation">,</span><br>    <span class="token property">"dev:livereload"</span><span class="token operator">:</span> <span class="token string">"livereload -e 'html,php' -p 1235"</span><span class="token punctuation">,</span><br>    <span class="token property">"build"</span><span class="token operator">:</span> <span class="token string">"parcel build ./src/assets.pug --public-url /dist/"</span><br>  <span class="token punctuation">}</span><br><span class="token punctuation">}</span></code></pre>

      <hr>
      <p>If you have any comments or feedback on this article, let me know! I'd love to hear your thoughts, go ahead and <a href="mailto:alistair@accudio.com">send me an email</a> at alistair@accudio.com or <a href="https://front-end.social/@accudio" rel="noopener noreferrer">contact me on Mastodon</a>.</p>
      <p>The post <a href="https://www.alistairshepherd.uk/writing/simple-build-tooling/">Simple, fast build tooling with live reload for a non-framework website</a> appeared first on <a href="https://alistairshepherd.uk">alistairshepherd.uk</a>.</p>
    ]]>
      </content>
    </entry>
  
    
    <entry>
      <title>JS library compilation to browser, esm and cjs using esbuild</title>
      <link href="https://www.alistairshepherd.uk/writing/js-library-bundling-esbuild/"/>
      <updated>2024-07-19T00:00:00Z</updated>
      <id>https://www.alistairshepherd.uk/writing/js-library-bundling-esbuild/</id>
      <content type="html">
        <![CDATA[
      <p>This is a quick post, public but mostly for myself about how to do simple JS library compilation and bundling with <a href="https://esbuild.github.io">esbuild</a>.</p>
<p>I have a few JS libraries that are shipped via npm, and can be used with an ESM <code>import</code>, CommonJS <code>require</code>, or a browser <code>script</code> element. I've found it pretty tricky in the past working out how to compile to all of these correctly from my ESM source, and the internet hasn't been very helpful either.</p>
<p>What I want is a setup that will produce all three of those with no huge legwork, handle bundling, sourcemaps, minification for me, convert modern syntax to es6, and have a dev mode that will re-compile on source change.</p>
<p>Esbuild is ideal for this circumstance as it has a simple API, all of those features and is extremely fast! I'm not saying this is the ideal setup but it works pretty well for me.</p>
<h2>build.js</h2>
<p>This is the build script, called with <code>node build.js</code> for a production build and <code>node build.js --watch</code> for dev mode. Pop these in your <code>package.json</code> scripts if you like.</p>
<p>You define the formats you want and the esbuild options within <code>buildAll</code>, I find this works well in most instances. This will get <code>src/index.js</code> and <code>src/script.js</code> (for browsers, imports index and runs it) and output to the <code>dist/</code> directory.</p>
<pre class="language-js"><code class="language-js"><span class="token keyword">import</span> esbuild <span class="token keyword">from</span> <span class="token string">'esbuild'</span><br><br><span class="token function">buildAll</span><span class="token punctuation">(</span><span class="token punctuation">)</span><br><br><span class="token keyword">async</span> <span class="token keyword">function</span> <span class="token function">buildAll</span><span class="token punctuation">(</span><span class="token punctuation">)</span> <span class="token punctuation">{</span><br>	<span class="token keyword">return</span> Promise<span class="token punctuation">.</span><span class="token function">all</span><span class="token punctuation">(</span><span class="token punctuation">[</span><br>		<span class="token function">build</span><span class="token punctuation">(</span><span class="token string">'script'</span><span class="token punctuation">,</span> <span class="token punctuation">{</span><br>			<span class="token literal-property property">entryPoints</span><span class="token operator">:</span> <span class="token punctuation">[</span><span class="token string">'src/script.js'</span><span class="token punctuation">]</span><span class="token punctuation">,</span><br>			<span class="token literal-property property">platform</span><span class="token operator">:</span> <span class="token string">'browser'</span><span class="token punctuation">,</span><br>			<span class="token literal-property property">minify</span><span class="token operator">:</span> <span class="token boolean">true</span><span class="token punctuation">,</span><br>			<span class="token literal-property property">target</span><span class="token operator">:</span> <span class="token punctuation">[</span><span class="token string">'es6'</span><span class="token punctuation">]</span><span class="token punctuation">,</span><br>		<span class="token punctuation">}</span><span class="token punctuation">)</span><span class="token punctuation">,</span><br>		<span class="token function">build</span><span class="token punctuation">(</span><span class="token string">'esm'</span><span class="token punctuation">,</span> <span class="token punctuation">{</span><br>			<span class="token literal-property property">entryPoints</span><span class="token operator">:</span> <span class="token punctuation">[</span><span class="token string">'src/index.js'</span><span class="token punctuation">]</span><span class="token punctuation">,</span><br>			<span class="token literal-property property">platform</span><span class="token operator">:</span> <span class="token string">'neutral'</span><br>		<span class="token punctuation">}</span><span class="token punctuation">)</span><span class="token punctuation">,</span><br>		<span class="token function">build</span><span class="token punctuation">(</span><span class="token string">'cjs'</span><span class="token punctuation">,</span> <span class="token punctuation">{</span><br>			<span class="token literal-property property">entryPoints</span><span class="token operator">:</span> <span class="token punctuation">[</span><span class="token string">'src/index.js'</span><span class="token punctuation">]</span><span class="token punctuation">,</span><br>			<span class="token literal-property property">target</span><span class="token operator">:</span> <span class="token punctuation">[</span><span class="token string">'node10.4'</span><span class="token punctuation">]</span><span class="token punctuation">,</span><br>			<span class="token literal-property property">platform</span><span class="token operator">:</span> <span class="token string">'node'</span><span class="token punctuation">,</span><br>		<span class="token punctuation">}</span><span class="token punctuation">)</span><span class="token punctuation">,</span><br>	<span class="token punctuation">]</span><span class="token punctuation">)</span><br><span class="token punctuation">}</span><br><br><span class="token keyword">async</span> <span class="token keyword">function</span> <span class="token function">build</span><span class="token punctuation">(</span><span class="token parameter">name<span class="token punctuation">,</span> options</span><span class="token punctuation">)</span> <span class="token punctuation">{</span><br>	<span class="token keyword">const</span> path <span class="token operator">=</span> <span class="token template-string"><span class="token template-punctuation string">`</span><span class="token interpolation"><span class="token interpolation-punctuation punctuation">${</span>name<span class="token interpolation-punctuation punctuation">}</span></span><span class="token string">.js</span><span class="token template-punctuation string">`</span></span><br>	console<span class="token punctuation">.</span><span class="token function">log</span><span class="token punctuation">(</span><span class="token template-string"><span class="token template-punctuation string">`</span><span class="token string">Building </span><span class="token interpolation"><span class="token interpolation-punctuation punctuation">${</span>name<span class="token interpolation-punctuation punctuation">}</span></span><span class="token template-punctuation string">`</span></span><span class="token punctuation">)</span><br><br>	<span class="token keyword">if</span> <span class="token punctuation">(</span>process<span class="token punctuation">.</span>argv<span class="token punctuation">.</span><span class="token function">includes</span><span class="token punctuation">(</span><span class="token string">'--watch'</span><span class="token punctuation">)</span><span class="token punctuation">)</span> <span class="token punctuation">{</span><br>		<span class="token keyword">let</span> ctx <span class="token operator">=</span> <span class="token keyword">await</span> esbuild<span class="token punctuation">.</span><span class="token function">context</span><span class="token punctuation">(</span><span class="token punctuation">{</span><br>			<span class="token literal-property property">outfile</span><span class="token operator">:</span> <span class="token template-string"><span class="token template-punctuation string">`</span><span class="token string">./dist/</span><span class="token interpolation"><span class="token interpolation-punctuation punctuation">${</span>path<span class="token interpolation-punctuation punctuation">}</span></span><span class="token template-punctuation string">`</span></span><span class="token punctuation">,</span><br>			<span class="token literal-property property">bundle</span><span class="token operator">:</span> <span class="token boolean">true</span><span class="token punctuation">,</span><br>			<span class="token literal-property property">logLevel</span><span class="token operator">:</span> <span class="token string">'info'</span><span class="token punctuation">,</span><br>			<span class="token literal-property property">sourcemap</span><span class="token operator">:</span> <span class="token boolean">true</span><span class="token punctuation">,</span><br>			<span class="token operator">...</span>options<span class="token punctuation">,</span><br>      <span class="token literal-property property">minify</span><span class="token operator">:</span> <span class="token boolean">false</span><br>		<span class="token punctuation">}</span><span class="token punctuation">)</span><br>		<span class="token keyword">await</span> ctx<span class="token punctuation">.</span><span class="token function">watch</span><span class="token punctuation">(</span><span class="token punctuation">)</span><br>	<span class="token punctuation">}</span><br>	<span class="token keyword">else</span> <span class="token punctuation">{</span><br>		<span class="token keyword">return</span> esbuild<span class="token punctuation">.</span><span class="token function">build</span><span class="token punctuation">(</span><span class="token punctuation">{</span><br>			<span class="token literal-property property">outfile</span><span class="token operator">:</span> <span class="token template-string"><span class="token template-punctuation string">`</span><span class="token string">./dist/</span><span class="token interpolation"><span class="token interpolation-punctuation punctuation">${</span>path<span class="token interpolation-punctuation punctuation">}</span></span><span class="token template-punctuation string">`</span></span><span class="token punctuation">,</span><br>			<span class="token literal-property property">bundle</span><span class="token operator">:</span> <span class="token boolean">true</span><span class="token punctuation">,</span><br>			<span class="token operator">...</span>options<span class="token punctuation">,</span><br>		<span class="token punctuation">}</span><span class="token punctuation">)</span><br>	<span class="token punctuation">}</span><br><span class="token punctuation">}</span><br></code></pre>
<h2>package.json</h2>
<p>To deploy this via npm and potentially load the browser version from a CDN like JSDelivr, we need to set up our <code>package.json</code> correctly. The following snippet is what I've found works well for modern versions of node. <code>exports</code> defines which version of the file to use based on how it's imported, Node will change which version is used based on whether we <code>require</code> or <code>import</code>. We set <code>main</code> to the <code>script.js</code> file so it's picked up by all major CDNs I've tried.</p>
<pre class="language-json"><code class="language-json"><span class="token punctuation">{</span><br>  <span class="token property">"main"</span><span class="token operator">:</span> <span class="token string">"./dist/script.js"</span><span class="token punctuation">,</span><br>	<span class="token property">"exports"</span><span class="token operator">:</span> <span class="token punctuation">{</span><br>		<span class="token property">"require"</span><span class="token operator">:</span> <span class="token string">"./dist/cjs.js"</span><span class="token punctuation">,</span><br>		<span class="token property">"import"</span><span class="token operator">:</span> <span class="token string">"./dist/esm.js"</span><span class="token punctuation">,</span><br>		<span class="token property">"default"</span><span class="token operator">:</span> <span class="token string">"./dist/esm.js"</span><br>	<span class="token punctuation">}</span><span class="token punctuation">,</span><br><span class="token punctuation">}</span></code></pre>

      <hr>
      <p>If you have any comments or feedback on this article, let me know! I'd love to hear your thoughts, go ahead and <a href="mailto:alistair@accudio.com">send me an email</a> at alistair@accudio.com or <a href="https://front-end.social/@accudio" rel="noopener noreferrer">contact me on Mastodon</a>.</p>
      <p>The post <a href="https://www.alistairshepherd.uk/writing/js-library-bundling-esbuild/">JS library compilation to browser, esm and cjs using esbuild</a> appeared first on <a href="https://alistairshepherd.uk">alistairshepherd.uk</a>.</p>
    ]]>
      </content>
    </entry>
  
    
    <entry>
      <title>Proxying an Image CDN with Cloudflare workers</title>
      <link href="https://www.alistairshepherd.uk/writing/cloudflare-worker-image-proxy/"/>
      <updated>2024-08-03T00:00:00Z</updated>
      <id>https://www.alistairshepherd.uk/writing/cloudflare-worker-image-proxy/</id>
      <content type="html">
        <![CDATA[
      <p>A short intro and code on how you can use Cloudflare workers on Cloudflare's free tier to proxy an image CDN like Cloudinary, CloudImage or Imgix. Using this you can load optimised images via the primary origin, support the Vary header for format conversion, and use Cloudflare's cache for those images.</p>
<h2>Why</h2>
<p>CDNs are extremely good for web performance, and if you can deliver your HTML via a CDN you can benefit from extremely fast from-cache load times and rely on a smaller server.</p>
<p>Cloudflare is not without it's problems, but it offers a free tier that gives you extremely good CDN capabilities for zero cost. In my opinion there are better options for bigger usecases, but for a small site the free Cloudflare tier is great.</p>
<p>One thing notaby absent from Cloudflare's free tier is image transformation. I find that super frustrating because loads of companies offer a free tier of image transformation but not Cloudflare, so if you want to keep costs down you're going to have to use a different company. If you're not familiar with Image CDNs and why you should use one, check out my <a href="/speaking/imagecdns/">talk on the subject</a>.</p>
<p>With an Image CDN you will probably get a URL something like <a href="http://kdnfwe948owqq.example.com">kdnfwe948owqq.example.com</a> that you can load your images through. Now you're loading your images through a different origin however, there is a performance implication where a new connection has to be established to that origin which may delay image loads.</p>
<p>Wouldn't it be great if we could use our image CDN, but make it use our primary origin that matches our website? It's a little tricky but turns out that's possible to do in the free tier of Cloudflare using Cloudflare workers!</p>
<h2>How</h2>
<p>This code is mostly taken from <a href="https://github.com/wesbos/cloudflare-cloudinary-proxy">Wes Bos</a> and <a href="https://github.com/wilsonhou/cloudflare-image-proxy">Wilson Hou</a> on GitHub, thanks to them for their work.</p>
<p>First thing is to set up a Worker. You can do this via a command-line but to be honest for this I just use the admin.</p>
<ol>
<li>Go to your Cloudflare Dashboard and &quot;Workers &amp; Pages&quot;</li>
<li>&quot;Create&quot;, &quot;Create Worker&quot;, name it, and then &quot;Deploy&quot;</li>
<li>Now lets' assign and test it</li>
<li>Assign it to a URL by going to your cloudflare zone/website and to &quot;Workers Routes&quot;</li>
<li>&quot;Add Route&quot;, choose the worker you've just created and create a route pattern
<ul>
<li>This needs to include the domain name and uses regex to specify patterns</li>
<li><a href="http://www.example.com/images/*">www.example.com/images/*</a> will make the worker serve all requests under the <code>/images/</code> path</li>
</ul>
</li>
<li>&quot;Save&quot; and confirm requests to <code>/images/whatever</code> return &quot;Hello world!&quot;</li>
</ol>
<p>Now we need to add the proxying code. To add this we go back to the worker we created and click &quot;Edit code&quot;. This opens an online editor with text field, debugging tools and the &quot;Deploy&quot; button in the top-right.</p>
<p>Add the below code, customise the <code>destination</code> variable and add any path manipulation you need. Then deploy and you should be golden!</p>
<pre class="language-js"><code class="language-js"><span class="token keyword">const</span> destination <span class="token operator">=</span> <span class="token string">'https://kdnfwe948owqq.example.com'</span><br><br><span class="token keyword">async</span> <span class="token keyword">function</span> <span class="token function">serveAsset</span><span class="token punctuation">(</span><span class="token parameter">request<span class="token punctuation">,</span> event<span class="token punctuation">,</span> context</span><span class="token punctuation">)</span> <span class="token punctuation">{</span><br>  <span class="token keyword">const</span> url <span class="token operator">=</span> <span class="token keyword">new</span> <span class="token class-name">URL</span><span class="token punctuation">(</span>request<span class="token punctuation">.</span>url<span class="token punctuation">)</span><br><br>  <span class="token comment">// if this is already in the cache return that</span><br>  <span class="token keyword">const</span> cache <span class="token operator">=</span> caches<span class="token punctuation">.</span>default<br>  <span class="token keyword">let</span> response <span class="token operator">=</span> <span class="token keyword">await</span> cache<span class="token punctuation">.</span><span class="token function">match</span><span class="token punctuation">(</span>request<span class="token punctuation">)</span><br>  <span class="token keyword">if</span> <span class="token punctuation">(</span>response<span class="token punctuation">)</span> <span class="token keyword">return</span> response<br><br>  <span class="token keyword">let</span> path <span class="token operator">=</span> url<span class="token punctuation">.</span>pathname<br>  <span class="token comment">// make any path manipulation here, eg removing a prefix</span><br>  <span class="token comment">// path = path.replace('/images', '')</span><br><br>  <span class="token comment">// request the URL with path and URL params</span><br>  <span class="token comment">// include request headers for content negotiation/auto-format</span><br>  response <span class="token operator">=</span> <span class="token keyword">await</span> <span class="token function">fetch</span><span class="token punctuation">(</span><br>    <span class="token template-string"><span class="token template-punctuation string">`</span><span class="token interpolation"><span class="token interpolation-punctuation punctuation">${</span>destination<span class="token interpolation-punctuation punctuation">}</span></span><span class="token interpolation"><span class="token interpolation-punctuation punctuation">${</span>path<span class="token interpolation-punctuation punctuation">}</span></span><span class="token interpolation"><span class="token interpolation-punctuation punctuation">${</span>url<span class="token punctuation">.</span>search<span class="token interpolation-punctuation punctuation">}</span></span><span class="token template-punctuation string">`</span></span><span class="token punctuation">,</span><br>    <span class="token punctuation">{</span> <span class="token literal-property property">headers</span><span class="token operator">:</span> request<span class="token punctuation">.</span>headers <span class="token punctuation">}</span><br>  <span class="token punctuation">)</span><br><br>  <span class="token keyword">const</span> headers <span class="token operator">=</span> <span class="token keyword">new</span> <span class="token class-name">Headers</span><span class="token punctuation">(</span>response<span class="token punctuation">.</span>headers<span class="token punctuation">)</span><br>  <span class="token comment">// add caching header, configured here for 1-year</span><br>  headers<span class="token punctuation">.</span><span class="token function">set</span><span class="token punctuation">(</span><span class="token string">"cache-control"</span><span class="token punctuation">,</span> <span class="token template-string"><span class="token template-punctuation string">`</span><span class="token string">public, max-age=31536000</span><span class="token template-punctuation string">`</span></span><span class="token punctuation">)</span><br>  <span class="token comment">// vary header so cache respects content-negotiation/auto-format</span><br>  headers<span class="token punctuation">.</span><span class="token function">set</span><span class="token punctuation">(</span><span class="token string">"vary"</span><span class="token punctuation">,</span> <span class="token string">"Accept"</span><span class="token punctuation">)</span><br><br>  <span class="token comment">// create response and add to the cache if successful</span><br>  response <span class="token operator">=</span> <span class="token keyword">new</span> <span class="token class-name">Response</span><span class="token punctuation">(</span>response<span class="token punctuation">.</span>body<span class="token punctuation">,</span> <span class="token punctuation">{</span> <span class="token operator">...</span>response<span class="token punctuation">,</span> headers <span class="token punctuation">}</span><span class="token punctuation">)</span><br>  context<span class="token punctuation">.</span><span class="token function">waitUntil</span><span class="token punctuation">(</span>cache<span class="token punctuation">.</span><span class="token function">put</span><span class="token punctuation">(</span>request<span class="token punctuation">,</span> response<span class="token punctuation">.</span><span class="token function">clone</span><span class="token punctuation">(</span><span class="token punctuation">)</span><span class="token punctuation">)</span><span class="token punctuation">)</span><br><br>  <span class="token keyword">return</span> response<br><span class="token punctuation">}</span><br><br><span class="token keyword">export</span> <span class="token keyword">default</span> <span class="token punctuation">{</span><br>  <span class="token keyword">async</span> <span class="token function">fetch</span><span class="token punctuation">(</span><span class="token parameter">request<span class="token punctuation">,</span> event<span class="token punctuation">,</span> context</span><span class="token punctuation">)</span> <span class="token punctuation">{</span><br>    <span class="token comment">// get the response</span><br>    <span class="token keyword">let</span> response <span class="token operator">=</span> <span class="token keyword">await</span> <span class="token function">serveAsset</span><span class="token punctuation">(</span>request<span class="token punctuation">,</span> event<span class="token punctuation">,</span> context<span class="token punctuation">)</span><br>    <span class="token comment">// if not a successful status code return response text</span><br>    <span class="token keyword">if</span> <span class="token punctuation">(</span><span class="token operator">!</span>response <span class="token operator">||</span> response<span class="token punctuation">.</span>status <span class="token operator">></span> <span class="token number">399</span><span class="token punctuation">)</span> <span class="token punctuation">{</span><br>      response <span class="token operator">=</span> <span class="token keyword">new</span> <span class="token class-name">Response</span><span class="token punctuation">(</span>response<span class="token punctuation">.</span>statusText<span class="token punctuation">,</span> <span class="token punctuation">{</span> <span class="token literal-property property">status</span><span class="token operator">:</span> response<span class="token punctuation">.</span>status <span class="token punctuation">}</span><span class="token punctuation">)</span><br>    <span class="token punctuation">}</span><br>    <span class="token keyword">return</span> response<br>  <span class="token punctuation">}</span><span class="token punctuation">,</span><br><span class="token punctuation">}</span><span class="token punctuation">;</span></code></pre>
<h2>Things to note</h2>
<p>This works pretty well, there are a couple of things to note however.</p>
<p>The free Cloudflare tier includes <a href="https://developers.cloudflare.com/workers/platform/limits/#worker-limits">limits on Workers</a>, so depending on the size of your site you may run into these. It's also account-level not website-level so keep that in mind. At time of writing it's pretty high at &quot;100,000 requests/day, 1000 requests/min&quot;. You'll likely run into that only if you have a busy site or many using this technique.</p>
<p>This also creates another layer of caching in your image chain, which depending on your setup and if your images are mutable could be tricky. This would probably already be the case for the image CDN however, so you'd be needing to clear two caches instead of one. Using permanent image URLs and ensuring modifications create a copy instead of modifying the original is a great way to avoid this being an issue and better for client caching also.</p>

      <hr>
      <p>If you have any comments or feedback on this article, let me know! I'd love to hear your thoughts, go ahead and <a href="mailto:alistair@accudio.com">send me an email</a> at alistair@accudio.com or <a href="https://front-end.social/@accudio" rel="noopener noreferrer">contact me on Mastodon</a>.</p>
      <p>The post <a href="https://www.alistairshepherd.uk/writing/cloudflare-worker-image-proxy/">Proxying an Image CDN with Cloudflare workers</a> appeared first on <a href="https://alistairshepherd.uk">alistairshepherd.uk</a>.</p>
    ]]>
      </content>
    </entry>
  
    
    <entry>
      <title>My response to UK CMA&#39;s potential remedies for mobile browser competition</title>
      <link href="https://www.alistairshepherd.uk/writing/cma-mobile-browsers-remedies/"/>
      <updated>2024-08-24T00:00:00Z</updated>
      <id>https://www.alistairshepherd.uk/writing/cma-mobile-browsers-remedies/</id>
      <content type="html">
        <![CDATA[
      <p>This is my personal submission to the the <a href="https://assets.publishing.service.gov.uk/media/66b484020808eaf43b50dea8/Working_paper_7_Potential_Remedies_8.8.24.pdf">Potential Remedies Working Paper</a> from the <a href="https://www.gov.uk/cma-cases/mobile-browsers-and-cloud-gaming">UK's Competition and Markets Authority regarding mobile browser competition</a>. Feel free to take inspiration/borrow from it and submit your own response by 29th August 2024. See <a href="https://open-web-advocacy.org/blog/uk-browser-and-cloud-investigation-may-fail-to-allow-web-app-competition/#we-need-your-help!-act-today!">Open Web Advocacy's article</a> for more info!</p>
<hr>
<p>In response to Competition and Markets Authority’s Mobile browsers and cloud gaming market investigation potential remedies.</p>
<p>I am a Web Developer resident in the UK, leading the development team for the London-based web agency Series Eight. This response is my personal concerns and comments rather than the position of any of my clients or employers, past or present. Series Eight is a website design and development agency that builds award-winning eCommerce and marketing websites for companies and brands within and outwith the UK. As a web developer at Series Eight I work with browsers and websites extensively and my comments come from my experience developing a large number of websites and web apps.</p>
<p>I have previously submitted my experiences and thoughts to the Competition and Markets Authority, and my responses are available in full on <a href="http://gov.uk">gov.uk</a>:</p>
<ul>
<li><a href="https://assets.publishing.service.gov.uk/media/622773fbd3bf7f1581a6eace/Developer_-_Alistair_Shepherd.pdf">Mobile Ecosystems Market Study — February 2022</a></li>
<li><a href="https://assets.publishing.service.gov.uk/media/6374dd43e90e07284de6da1e/Alistair_Shepherd_-_Consultation_response_-_Publication_version.pdf">Mobile browsers and cloud gaming MIR consultation — July 2022</a></li>
</ul>
<p>These responses detail the difficulties that developers like myself face when working with Safari, the impact that the lack of iOS browser competition has on developers and businesses, and the importance and current limitations of Progressive Web Apps on Android and iOS.</p>
<p>I commend and appreciate the work and remedies proposed by the CMA, and believe they make a significant impact towards reducing monopolies by device vendors and opening up web access and capabilities.</p>
<p>I am however concerned by an area not covered by the proposals which may provide a way for Apple to follow the proposals whilst violating their intention.<br>
The proposals make the recommendation that Apple opens up equivalent access to other browsers equal to their Safari browser. My concern with this is that Apple has regularly shown that it it willing to hamper the functionality of Safari browser if it suits their other interests. We saw this earlier this year, where for EU users Apple disabled progressive web app installation within Safari for all users, before eventually backtracking due to public pressure.</p>
<p>This is commonly thought to be due to Apple's conflict as a browser vendor and also an app store provider. By limiting the functionality available to web software via Safari, it encourages developers to build iOS-specific apps sold and distributed via the App Store where app-store fees can be imposed.</p>
<p>My concern is that if browser functionality is pegged against Safari functionality, Apple will effectively kill Progressive Web Apps by hampering Safari rather than allow third-party browsers to enable more competition against native apps from the web. Apple could prevent other browsers from installing web apps to the home screen, or limit the access to Software and Hardware APIs to significantly weaken the capabilities and usefulness of web apps.</p>
<p>The success of Progressive Web Apps is in my opinion essential to opening up access and competition to mobile devices. Progressive Web Apps would all app creation to have a lower barrier to entry, lower fees, and allow for more interoperability between devices.<br>
In my capacity in working for the web within a small agency, Progressive Web Apps are a vital tool allowing businesses and agencies to produce better user experiences at a more affordable cost than native apps. The success of Progressive Web Apps through both Safari and third-party browsers would be hugely impactful to web developers, agencies and businesses where native app development would be prohibitively expensive. This allows big international companies with higher budgets to maintain entrenched advantages on iOS via the app store.</p>
<p>Open Web Advocacy — a group I am a member of — has proposed four additional remedies focused on ensuring that the web can compete fairly on iOS and that Apple is unable to get around the proposed remedies as I have described:</p>
<ol>
<li>&quot;Apple shall allow third-party browsers to install and manage Web Apps using their own browser engine.&quot;</li>
<li>&quot;A requirement for Apple to implement Install Prompts for iOS Safari.&quot;</li>
<li>&quot;A requirement for Apple to grant all software and hardware access to APIs to browsers using alternative browser engines that they require to port their engines and implement stability, functionality, security and privacy. Restrictions on this can be subject to only strictly necessary, proportionate and justified security grounds.&quot;</li>
<li>&quot;Where feature parity between Web Apps and Native Apps is possible, Apple must technically enable it and it should not be artificially prevented either by OS rules or OS design. Apple must not self-preference their own Apps, Apps sold via their App Store or their own Services over Web Apps.&quot;</li>
</ol>
<p>I believe these remedies would be sufficient to ensure iOS is a platform that does not limit and hamper the web. Particularly remedies 3 and 4 I think are vital to ensuring it is extremely clear that Apple must provide equal access to web apps and cannot continue to limit them in order to prefer and push their app store and native apps.</p>
<p>My comments are of a personal capacity and do not represent any organisation I work for. I would like my response to be attributed to me by name, and you have my permission to publish or quote from this document with or without attribution.</p>
<p>Best Regards,<br>
Alistair Shepherd</p>

      <hr>
      <p>If you have any comments or feedback on this article, let me know! I'd love to hear your thoughts, go ahead and <a href="mailto:alistair@accudio.com">send me an email</a> at alistair@accudio.com or <a href="https://front-end.social/@accudio" rel="noopener noreferrer">contact me on Mastodon</a>.</p>
      <p>The post <a href="https://www.alistairshepherd.uk/writing/cma-mobile-browsers-remedies/">My response to UK CMA&#39;s potential remedies for mobile browser competition</a> appeared first on <a href="https://alistairshepherd.uk">alistairshepherd.uk</a>.</p>
    ]]>
      </content>
    </entry>
  
    
    <entry>
      <title>Selling a small front-end web project — what I learned</title>
      <link href="https://www.alistairshepherd.uk/writing/selling-web-project/"/>
      <updated>2024-09-16T00:00:00Z</updated>
      <id>https://www.alistairshepherd.uk/writing/selling-web-project/</id>
      <content type="html">
        <![CDATA[
      <p>In 2017, I made a small web app for interacting with the Spotify API — mostly to learn some new technologies. Surprisingly it took off, getting quite a lot of visitors despite my lack of interest in developing it further or monetising it.</p>
<p>Earlier this year someone reached out to me asking about my plans for the project and whether I would be interested in selling it. Fast forward a few weeks and I have sold my first web project and learnt a lot in the meantime!</p>
<p>I didn't find much information online about the process of selling a project, particularly one without any monetisation and at a small scale. This is my attempt at helping others who may end up in this position.</p>
<p>That's the TLDR; stick around for more detail!</p>
<h2>About My Top for Spotify</h2>
<p>So the project is called <a href="https://mytopspotify.com">My Top for Spotify</a>. It's a pretty small web app built on React that allows a user to connect their Spotify account and see what the Spotify API says about their top artists and songs over a few different time periods.</p>
<p>In early 2017 I was looking to move from freelance development into a permanent role, and loads of jobs were looking for React experience. I'd done the standard hello world and to-do app, but wanted something live, real-world, and app-like for the experience and a portfolio piece. I built it with Create React App and a server based on AWS Lambda and Serverless. To be honest I really don't like the tech stack, and it was the start of my dislike of those two products — but that's not what this post is about!</p>
<p>I cracked it out in a few weeks, the functionality is pretty straightforward and the design has some cool CSS details but overall unpolished. But it was just a demo for me and a few friends so who cares right? I named it My Top for Spotify, purchased a .com and .io domain and deployed, shared it around some friends and family and that was that.</p>
<p>Quite quickly it started to get quite a lot of traffic, ranking highly on search engines, and being mentioned in blogs and YouTube videos. I believe this is because just a few months earlier Spotify released their first Spotify Wrapped — an annual review of your Spotify listening. This seemed to drive loads of interest in seeing this kind of data, and my little experiment ended up top of the search results for queries like &quot;See top Spotify tracks&quot;.</p>
<p>And then I basically did nothing! The app got about 4 million annual visitors in those first few years, and I just left it. I considered further work and monetisation but didn't have much interest in that — particularly not in turning it into a business. I paid the hosting and domain costs for it myself, did the odd urgent fix or upgrade and otherwise let it be. After that initial deploy I probably spent about 10 hours on it total over a period of 7 years.</p>
<p>That's where it sat as of May 2024, getting about 1–2 million views a year.</p>
<h2>Thinking about selling</h2>
<p>I received an email out of the blue earlier this year, asking if I had any plans for the project and if I would consider selling it. I was pretty confident this was a spam email to be honest — perhaps I'm too suspicious! Regardless, I replied with mild interest and frankly saying it was something I hadn't even considered.</p>
<p>What followed was a lot of emails back-and-forth, discussing the project and their interest. The whole process went pretty quick, even despite a time difference between the me in the UK and the buyers in Australia. Within six weeks of that initial email we were making the transfer.</p>
<p>After those first few emails I became very aware I had no idea what I was doing. Searching online brought up some guides and articles, but only a handful that were relevant to a small-scale project that doesn't earn any money. <a href="https://training.kalzumeus.com/newsletters/archive/selling_software_business">Patrick McKenzie</a> has a good article which I would suggest reading, especially for selling a small SaaS.</p>
<p>I am also fortunate to have some great people in my 'network' that gave me advice, including my parents, folk I work with at Series Eight, and online friends on Mastodon and Discord. A particularly huge thank you to <a href="https://narrativ.es/@janl">Jan Lehnardt</a>, who kindly spent some time on a call taking me through his experience and helping me work through my priorities and the risks involved. <a href="https://kilianvalkhof.com">Kilian Valkhof</a> also gave me much-appreciated advice from selling a similar size of project.</p>
<h2>Why sell, and my priorities</h2>
<p>The main reason I was willing to sell My Top Spotify was because it was going to waste under me. I invested close to nothing into it — AWS and domain costs and the odd hour when there was an SSL cert issue or Netlify needed an update.</p>
<p>I had no intention of doing anything further with it, and there is now more competition in the area — companies actually investing in their products to add new features and gain more visibility. That resulted in a decline in popularity to other tools investing in features and marketing. Ultimately I didn't give or receive much value, and people willing to invest time and money could do a lot more with it.</p>
<p>Because I wasn't planning on selling it originally, that gave me a bit of power over negotiations and the decision — at least in my head. If I decided that I wasn't happy with the arrangement I wouldn't be any worse off — so I could be picky.</p>
<p>On being picky, it was very important for me to consider my priorities for the project and for the sale:</p>
<ul>
<li>The buyer should appear to care about the project;</li>
<li>I had to be confident it wouldn't be immediately strip-mined of value or users exploited maliciously;</li>
<li>A fair offer (more on that shortly);</li>
<li>No major risk from me — money before permanent transfers;</li>
<li>Any additional fees covered by the buyer – common but not standard.</li>
</ul>
<p>Those were the principles I decided I wasn't willing to compromise on, and I think that was extremely helpful for me to focus on.</p>
<p>I've seen and heard of web projects being strip-mined of value, becoming riddled with ads, or even injecting malware. I really worried about that, and it was the main thing that held me back.</p>
<p>As for the money and risk, as someone new to all this making sure I wasn't about to screw up was really important. Jan was fantastic helping me work through the risks I was and wasn't comfortable with.</p>
<h2>How do I value an unmonetised side project?</h2>
<p>Okay so I had a stable project with lots of visitors, great search engine ranking, low outgoings, and a pretty promising future.</p>
<p>So what is it worth?</p>
<p>Wherever I looked online, the first step to answering that was &quot;how much does it earn?&quot;. At a big fat zero that makes it tricky! I tried loads of domain evaluators — ranging from $100–$100k — and the calculators of various project 'marketplaces' do not handle that situation well at all. I could find essentially no good blogs or details that accounted for selling a free project based on potential rather than revenue.</p>
<p>Eventually I gave up and decided to ask the buyer &quot;How much are you going to pay for it?&quot;, and see what happens. Their suggestion was what we based our final amount on — assuming the site become advertising-funded and basing the value on <a href="https://adsense.google.com/start/#calculator">Google's AdSense estimator</a>. With some negotiation we agreed on how many years to include, and to using an average monthly pageviews from the previous year of my analytics to account for seasonal shifts.</p>
<p>I'm not a salesperson or valuator so I'm sure others would use a different method accounting for repeat visitors, SEO potential, backlinks, domain value and much more. Probably with WAY different numbers. Crucially however I am really happy with this method. The key thing for me is that it's very clear, independent, and objective.</p>
<p>We negotiated details but in the end with the AdSense estimator I could easily weigh up the comparison between implementing ads myself and receiving ongoing funds, or receiving them up-front without the need of ongoing maintenance. I'm sure that there was potential for further valuation but this method felt fair to me and the buyer.</p>
<p>I'm not going to detail any of the final numbers we agreed as that's not the point of this. If you're selling something similarly unmonetised then I'd recommend having a discussion with the buyer and see where it goes. The AdSense tool could be handy to reference if that fits the project.</p>
<h2>The mechanics of selling</h2>
<p>I've decided I'm selling, we've agreed a price, what happens next?</p>
<p>There were a few things I was unsure about at this point: do we need to use a broker; do I need a solicitor to review contracts; how do we handle AUD → GBP payment?</p>
<p>I had absolutely no idea if a broker would be needed, what it costed and involved, but lots of places online swear by them. Both Jan and Kilian talked to me about the risks vs costs, so combined with the principles I detailed above there were a couple options:</p>
<ul>
<li>We use a broker for the domain names, but the buyer pays for it;</li>
<li>I transfer everything non-permanent — codebase, re-assign DNS, environment variables — and then only transfer permanent domain ownership once I've received payment.</li>
</ul>
<p>Obviously this depends on the buyer but this second option worked out great for us. It reduced the complexity and fees whilst giving me control and some protection against the risk of things going wrong. From the buyer's perspective there was still some risk but by transferring everything non-permanent I'd proven I was serious about following through.</p>
<p>The contracts discussion was pretty easy also, I didn't need a solicitor and went through it myself — asking a couple friends to check there wasn't anything I was missing. Once again your mileage may vary.<br>
I asked for a few additions — making it clear I wasn't liable for future support, clarifying trademark details, and making sure I was able to write this blog post! All were agreed and easily added.</p>
<p>And finally for international payments, turns out that's an easy one. The buyer took my UK bank account details and used Wise to transfer the amount agreed over at the market exchange rate. Nice and easy!</p>
<p>Once those questions were worked out it went pretty smoothly — the code was already open-source so I had no issue handing that over before we'd signed, what had real value was the domain name. I detailed the tech stack and how it was built, wrote some documentation and tips on ongoing maintenance.</p>
<p>We hopped on a call to finalise the tech setup, answer any technical questions, share environment variables, sign the contract, and finally switch the DNS. Once that was all sorted they sent the payment, and when I confirmed that in my bank account I started the domain transfer.</p>
<p>And that was it! We had a few emails in the following weeks to confirm everything was going well, share Google Search Console and Analytics access and that was that.</p>
<h2>How does it feel?</h2>
<p>It feels good! It was a good experience to go through, I feel like I'm doing good by the project that I had basically abandoned, I appreciate the money of course, and it's also a much appreciated new marker of success for me. I'll be sewing on my &quot;website sale&quot; scout badge and putting &quot;Founder with one major exit&quot; on my CV now 😉.</p>
<p>I feel quite fortunate that the people who have bought the project were very helpful and friendly throughout, and seem genuinely keen to develop and improve the project. I'm sure that not all sales go as well and smoothly so I am thankful for that.</p>
<h2>My key takeaways</h2>
<p>If you take anything from this post — and for my own future reference — here are my takeaways:</p>
<ul>
<li>Work out your priorities, they help you make judgements about process and value;</li>
<li>You don't necessarily need a broker, you may be able do it without one;</li>
<li>Unmonetised projects are hard to value, ask for offers. The AdSense Estimator may be helpful;</li>
<li>Consider adding analytics to projects. It doesn't have to be GA, but page views at minimum are essential for a sale;</li>
<li>Make sure contracts make clear your support/responsibility post-sale;</li>
<li>Remember that ultimately as soon as it's sold it's out of your control;</li>
<li>Don't use an io domain! Read <a href="https://www.beep.blog/io/">.io considered harmful</a>, but also they're more expensive than they're worth for side projects.</li>
</ul>
<h2>What now?</h2>
<p>For My Top for Spotify, I don't really know! There has clearly been some hard work and new functionality like global stats already, and I've been told that they're working hard to evolve and improve it.<br>
I think I'll check in once in a while for the sake of curiosity, but I'm happy to otherwise move on. I wish the new team working on the site all the best with it. If you use Spotify I'd encourage you to check it out at <a href="https://mytopspotify.com">mytopspotify.com</a> if you're interested.</p>
<p>For me nothing changes. I have no interest in building things with only value in mind, so it'll continue to be things for myself and for fun!</p>

      <hr>
      <p>If you have any comments or feedback on this article, let me know! I'd love to hear your thoughts, go ahead and <a href="mailto:alistair@accudio.com">send me an email</a> at alistair@accudio.com or <a href="https://front-end.social/@accudio" rel="noopener noreferrer">contact me on Mastodon</a>.</p>
      <p>The post <a href="https://www.alistairshepherd.uk/writing/selling-web-project/">Selling a small front-end web project — what I learned</a> appeared first on <a href="https://alistairshepherd.uk">alistairshepherd.uk</a>.</p>
    ]]>
      </content>
    </entry>
  
    
    <entry>
      <title>My day at State of the Browser 2024</title>
      <link href="https://www.alistairshepherd.uk/writing/state-of-the-browser-2024/"/>
      <updated>2024-09-19T00:00:00Z</updated>
      <id>https://www.alistairshepherd.uk/writing/state-of-the-browser-2024/</id>
      <content type="html">
        <![CDATA[
      <p>Last weekend I was at State of the Browser in London. It’s a fantastic conference, focusing on web development and many social issues in our industry. The talks were brilliantly curated and organised, and it was overall a brilliant day. In this post I’ll share my thoughts from the day and my takeaways. The talks will be available online in the new few months if any catch your attention!</p>
<h2>Sustainable Futures: Funding the Web Ecosystem</h2>
<p>The first talk of the day was from <strong>Stephanie Stimac</strong>, discussing how the browser and web ecosystem is perhaps a little more fragile than it would at first appear. My job, our entire industry, and in fact pretty much everyone’s lives all rely on web browsers. These browsers are developed, owned and funded by mega-corporations like Google and Apple. Even Mozilla with Firefox is mostly funded by Google.</p>
<p>Stephanie took us through a thought experiment — what would happen to Chrome if Google disappeared? It may be open-source but it turns out browsers are <strong>extremely</strong> complex and difficult to make!</p>
<p>It’s a scary thought that the basis of the web is all funded by hyper-capitalist corporations. The key takeaway was to encourage us to have discussions over how we can fund browsers — and open-source software more generally — in a more sustainable way.</p>
<p>It was a great start for the day, criticising capitalism and a couple Dune references!</p>
<h2>No industry for old men</h2>
<p><strong>Steve Faulkner</strong> followed, with an extremely funny and chaotic run-through of the process of Accessibility standards. The accessibility issues in question were interesting to hear about, but my main takeaway was an insight into the history of web standards, and some really fascinating insights into the web industry in its early years. He gave some great advice and detail over how the W3C works and how anyone can contribute to web and accessibility standards. He also made many jokes at the expense of his friends in the industry — “without naming any names… Joe” — which was very entertaining!</p>
<h2>The mazy web she whirls: starting Open Web Advocacy</h2>
<p><strong>Stuart Langridge</strong> took the stage next with a talk about Open Web Advocacy, speaking with legislators to hold big tech companies to account.</p>
<p>I’ve seen a fair few conference talks and let me tell you — this was one of the best. Stuart did an absolutely <strong>beautiful</strong> job ‘weaving’ together the normally quite dry topic of web legislation, with the Lord Tennyson poem “The Lady of Shallot”:</p>
<blockquote>
<p>Out flew the web, and floated wide, The mirror cracked from side to side, &quot;The curse is come upon me,&quot; cried The Lady of Shalott.</p>
</blockquote>
<p>I almost don’t want to spoil how he does it, because it was an absolute delight you should experience yourself when the recording comes out! He was opening up the quite inaccessible topic of browser competition and functionality restriction to be more entertaining and understandable, which is the goal of Open Web Advocacy.</p>
<p>The development side of the talk was sharing the story of how Open Web Advocacy came to be, and how it ended up representing a huge number of independent developers and speaking to governments about web competition. Lots of gems like how all iOS browsers are actually Safari, how Android limits certain functionality to Chrome, and how Apple is fighting against the power and independence of the web.</p>
<p>Check out the talk when it comes out and give your support to <a href="https://open-web-advocacy.org/">Open Web Advocacy</a>!</p>
<h2>Web Components: Little Bits</h2>
<p>Our final talk of the morning was from <strong>David Darnes</strong>, about implementing Web Components. It was a great introduction to how Web Components can be used to implement small functional pieces of JavaScript on a page. I’m a big fan of this kind of talk (I’ve done one myself!), as it adds a bit of code to the day that’s really easy to follow along, and super practical to our work. Dave’s example was about the little speaker on <a href="https://darn.es">his website</a> that says his name — often by his dad.</p>
<p>After the talk I’m excited to use Web Components a little more!</p>
<h2>Around the wwworld: Web MIDI, Web AUDIO and what the web does best</h2>
<p>Oh my goodness <strong>Katie Fenn</strong>’s talk was such good fun! She used web APIs like WebMIDI and WebAudio to interact with a MIDI-enabled synthesiser to recreate Daft Punk’s “Around the world”. Using a browser!!!</p>
<p>It was a great talk, we were all bopping away to the music, learning about how to make EDM, synthesisers, MIDI, and audio on the web.</p>
<p>I’d recommend watching the talk when it comes out, but Katie has recorded it on YouTube also. Just remember this is all controlled and produced by a browser! Katie also has <a href="https://www.youtube.com/watch?v=E9zqxaaC4lA">a recording on YouTube</a>.</p>
<h2>Code Your Future</h2>
<p><strong>Gayle Ngozi</strong> took to the stage to talk about <a href="https://codeyourfuture.io">Code your Future</a>, which works with disadvantaged people and refugees to teach skills in development. It was a great talk, focusing on how difficult it can be in those situations to get a job and how tech and coding can be a way out of that. The takeaway was how we can all help make an impact by contributing time, resources, and hiring these fantastic developers.</p>
<h2>Improving Accessibility with ARIA-AT: A Web Standards Case Study</h2>
<p>Our second talk of the day about web standards, <strong>Lola Odelola</strong> took us in detail over how the W3C operates including Working Groups and Community Groups. This was in the context of working through a standard for automated testing of Accessibility Technologies. It was really interesting hearing about how the W3C works, and about such an important topic also.</p>
<h2>Fluid typography (and its role in design systems)</h2>
<p>Richard Rutter — founder of the wonderful <a href="https://clearleft.com">Clearleft</a> — finished the day with a slightly more design-focused talk. His talk was about fluid typography, how you can use fluid type systems to build a more consistent type experience based on screen size without harsh breakpoints.</p>
<p>We use fluid typography in our work already when appropriate, but Richard presented some really interesting tools and best practices — see <a href="https://utopia.fyi">utopia.fyi</a> — which I’m excited to share with the rest of the Series Eight team!</p>
<h2>Summing up</h2>
<p>It was an absolutely brilliant day, and flew by. The State of the Browser team always puts on a fantastic event, and a big thank you to them and the speakers for such a high quality conference. So my key takeaways:</p>
<ul>
<li>Discuss alternative funding models for browsers;</li>
<li>Participate in web accessibility standards (chaotic);</li>
<li>Join Open Web Advocacy and claim the web for us;</li>
<li>Build web components;</li>
<li>Make music with browsers 🎶 and try not buy a synthesiser — seriously Alistair, you don’t need one;</li>
<li>Support non-profit organisations making the tech industry more accessible;</li>
<li>Participate in web accessibility standards (serious);</li>
<li>Use fluid type!</li>
</ul>

      <hr>
      <p>If you have any comments or feedback on this article, let me know! I'd love to hear your thoughts, go ahead and <a href="mailto:alistair@accudio.com">send me an email</a> at alistair@accudio.com or <a href="https://front-end.social/@accudio" rel="noopener noreferrer">contact me on Mastodon</a>.</p>
      <p>The post <a href="https://www.alistairshepherd.uk/writing/state-of-the-browser-2024/">My day at State of the Browser 2024</a> appeared first on <a href="https://alistairshepherd.uk">alistairshepherd.uk</a>.</p>
    ]]>
      </content>
    </entry>
  
    
    <entry>
      <title>Spring and Summer 2024 — Life, links and lmedia</title>
      <link href="https://www.alistairshepherd.uk/writing/spring-summer-2024/"/>
      <updated>2024-09-22T00:00:00Z</updated>
      <id>https://www.alistairshepherd.uk/writing/spring-summer-2024/</id>
      <content type="html">
        <![CDATA[
      <p>I've had a pretty crazy 2024 so far — but to be honest I kinda say that all the time, maybe that's just my life? 😅</p>
<p>Anyway, wanted to do a wee summary of what I've been up to, blog posts I've written and read, and media!</p>
<h2>Life</h2>
<p>Work recently has been pretty busy. We've had a big new job come up that's a lot more technical and involved than most of our work, so that's taken a lot of attention. I've also been getting more into dev management — performance reviews, hiring practices, standards, etc.</p>
<p>I've also dedicated a lot of attention to selling My Top for Spotify, a project I built a few years ago. Check out my blog post &quot;<a href="/writing/selling-web-project/">Selling a small front-end web project — what I learned</a>&quot; for more info on that. The short version is that it was exciting, new, and took a lot of my focus for about 6 weeks!</p>
<p>Personal life and hobbies have taken more of a backseat than I'd have liked.</p>
<ul>
<li>We've had friends come to stay a few times — I <strong>love</strong> hosting people;</li>
<li>Saw <a href="https://nightflightofficial.com">Night Flight</a> and <a href="https://thejapanesehouse.co.uk">The Japanese House</a> — live</li>
<li>Went to Edinburgh Pride;</li>
<li>Did a few hillwalks — less than I'd like but it's tricky without a car;</li>
<li>Sat on a mountain with some friends for hours watching the perseid meteor shower;</li>
<li>Spent a few days visiting family in the Highlands;</li>
<li>Had a good Edinburgh fringe with 5 shows, a couple great ones;</li>
<li>Went to the south of France near Marseille for a week.</li>
</ul>
<p>I launched my <a href="https://ko-fi.com/alistairshepherd">ko-fi page selling stickers and my art prints</a>. The stickers have done well but the art prints less so. I'm not surprised, I haven't really pushed those much so need to do more.</p>
<p>I'm also regularly attending the <a href="https://events.indieweb.org">Homebrew Website Club</a> meetup in Edinburgh. It's a good small group and a great chance to casually chat and catch up about the web!</p>
<p>Last weekend I went to the State of the Browser conference down in London which is always fantastic. The talks were exceptional this year and I have great fun meeting up with and making new friends.</p>
<h2>Posts from me</h2>
<p>I've had a pretty good few months for blogging! What a great opportunity to encourage you to read and share those!</p>
<ul>
<li><a href="/writing/simple-build-tooling/">Simple, fast build tooling with live reload for a non-framework website</a></li>
<li><a href="/writing/js-library-bundling-esbuild/">JS library compilation to browser, esm and cjs using esbuild</a></li>
<li><a href="/writing/cloudflare-worker-image-proxy/">Proxying an Image CDN with Cloudflare workers</a></li>
<li><a href="/writing/cma-mobile-browsers-remedies/">My response to UK CMA's potential remedies for mobile browser competition</a></li>
<li><a href="/writing/selling-web-project/">Selling a small front-end web project — what I learned</a></li>
<li><a href="/writing/state-of-the-browser-2024/">My day at State of the Browser 2024</a></li>
</ul>
<h2>Posts from others I've enjoyed</h2>
<p>I recently got back into RSS and reading blogs! I had 2500+ unread articles in my feed reader so declared RSS bankruptcy back in July, but have been keeping up since. It's been great to be reading more and I feel more creative, inspired, and excited about the web thanks to it!</p>
<p>Here are a list of some fantastic articles I've read since May, in order of when they were posted:</p>
<ul>
<li><a href="https://blog.glyphdrawing.club/font-with-built-in-syntax-highlighting/">Font with Built-In Syntax Highlighting — Glyph Drawing Club</a></li>
<li><a href="https://maphappenings.com/2024/07/18/mapquest/">12 Map Happenings that Rocked our World: Part 10 — James Killick</a></li>
<li><a href="https://heydonworks.com/latest/">Heydon Pickering's HTML element descriptions</a></li>
<li><a href="https://www.oddbird.net/2024/08/13/csswg-08/">Partial Feature Queries, Relaxed Layout Containment, and More — Miriam Suzanne</a></li>
<li><a href="https://csswizardry.com/2024/08/blocking-render-why-whould-you-do-that/">blocking=render: Why would you do that?! — Harry Roberts</a></li>
<li><a href="https://chriscoyier.net/2024/08/14/there-are-two-kinds-of-advertising/">There are two kinds of advertising — Chris Coyier</a></li>
<li><a href="https://csswizardry.com/2024/08/cache-grab-how-much-are-you-leaving-on-the-table/">Cache Grab: How Much Are You Leaving on the Table? — Harry Roberts</a></li>
<li><a href="https://darthmall.net/notes/2024/some-git-tips/">Some Git Tips — Evan Sheehan</a></li>
<li><a href="https://www.matuzo.at/blog/2024/100daysof-day109">Day 109: the animation-composition property — Manuel Matuzović</a></li>
<li><a href="https://www.joshtumath.uk/posts/2024-08-21-my-first-week-in-the-css-working-group/">My first week in the CSS Working Group — Josh Tumath</a></li>
<li><a href="https://dgerrells.com/blog/can-you-convert-a-video-to-pure-css">Can you convert a video to pure css? — David Gerrells</a></li>
<li><a href="https://brucelawson.co.uk/2024/web-apps-on-ios-is-the-cma-missing-the-point/">Web Apps on iOS: is the CMA missing the point? — Bruce Lawson</a></li>
<li><a href="https://eieio.games/essays/the-secret-in-one-million-checkboxes/">The secret inside One Million Checkboxes — Nolen Royalty</a></li>
<li><a href="https://adrianroselli.com/2024/08/mountain-chicken.html">Mountain Chicken! — Adrian Roselli</a></li>
<li><a href="https://adrianroselli.com/2024/09/under-engineered-estimated-reading-time-feature.html">Under-Engineered Estimated Reading Time Feature — Adrian Roselli</a></li>
<li><a href="https://daverupert.com/2024/09/dev-tools-performance-monitor-panel/">The Dev Tools Performance Monitor Panel — Dave Rupert</a></li>
<li><a href="https://modem.io/blog/blog-monetization/">How to Monetize a Blog — modem.io</a></li>
<li><a href="https://mxb.dev/blog/buildless/">Going Buildless — Max Böck</a></li>
<li><a href="https://philipwalton.com/articles/the-state-of-es5-on-the-web/">The State of ES5 on the Web — Philip Walton</a></li>
<li><a href="https://adactio.com/journal/21405">What price? — Jeremy Keith</a></li>
<li><a href="https://www.zachleat.com/web/symbiotic-open-source/">Open Source Needs to be Financially Symbiotic — Zach Leatherman</a></li>
<li><a href="https://daverupert.com/2024/09/good-forms/">Good forms — Dave Rupert</a></li>
<li><a href="https://css-irl.info/limitation-breeds-creativity/">Limitation Breeds Creativity: A Study in Composition with Custom Properties — Michelle Barker</a></li>
<li><a href="https://codersblock.com/blog/making-orbit-animations-with-css-custom-properties/">Making Orbit Animations with CSS Custom Properties — Will Boyd</a></li>
<li><a href="https://csswizardry.com/2024/09/optimising-for-high-latency-environments/">Optimising for High Latency Environments — Harry Roberts</a></li>
</ul>
<h2>Media</h2>
<p>I felt like I hadn't [read|watched|listened|played] as much media as I would've normally, but actually listing them out is quite a few! It didn't feel like much though!</p>
<h3>Music</h3>
<ul>
<li><a href="https://album.link/gb/i/1698723205">Chappel Roan — The Rise and Fall of a Midwest Princess</a></li>
<li><a href="https://album.link/gb/i/1750691190">HONNE — OUCH</a></li>
<li><a href="https://album.link/gb/i/1733061411">Cage the Elephant — Neon Pill</a></li>
<li><a href="https://album.link/gb/i/1741749101">Night Flight — English Noise</a></li>
<li><a href="https://album.link/gb/i/1712730224">Bleachers — Bleachers</a></li>
</ul>
<h3>Movies/TV/YouTube</h3>
<ul>
<li><a href="https://www.imdb.com/title/tt16426418">Challengers</a></li>
<li><a href="https://www.youtube.com/@jetlagthegame">Jet Lag: The Game</a></li>
<li><a href="https://www.imdb.com/title/tt1024943">Om Shanti Om</a></li>
<li><a href="https://www.imdb.com/title/tt6587046">The Boy and the Heron</a></li>
<li><a href="https://www.imdb.com/title/tt21232992">Theater Camp</a></li>
<li><a href="https://www.imdb.com/title/tt1888075">Death in Paradise</a></li>
</ul>
<h3>Games</h3>
<p>I also now track my games on Infinite Backlog if you're interested or on there! I also ranked every game I've ever played* and produced a top 100** which was good fun! Here's the recent ones:</p>
<ul>
<li><a href="https://infinitebacklog.net/games/agatha-christie-murder-on-the-orient-express--1">Agatha Christie: Murder on the Orient Express</a></li>
<li><a href="https://infinitebacklog.net/games/pentiment">Pentiment</a></li>
<li><a href="https://infinitebacklog.net/games/nuts">Nuts</a></li>
<li><a href="https://infinitebacklog.net/games/sherlock-holmes-chapter-one">Sherlock Holmes: Chapter One</a></li>
<li><a href="https://infinitebacklog.net/games/lego-builders-journey">LEGO Builder's Journey</a></li>
<li><a href="https://infinitebacklog.net/games/the-room--1">The Room</a></li>
<li><a href="https://infinitebacklog.net/games/cultist-simulator">Cultist Simulator</a> — it's finally clicked after 6 years!</li>
<li><a href="https://infinitebacklog.net/games/one-night-burlesque">One Night Burlesque</a></li>
</ul>
<p>* at least as far as I remember<br>
** not quite 100 but close enough!</p>
<h2>Photos</h2>
<div class="auto-grid" style="--auto-grid-fit:auto-fit;--auto-grid-min-size:22rem;">
  <img class="rounded" src="/images/nairn-beach.jpg?width=528,height=337.92" srcset="/images/nairn-beach.jpg?width=400,height=256 400w, /images/nairn-beach.jpg?width=600,height=384 600w, /images/nairn-beach.jpg?width=800,height=512 800w, /images/nairn-beach.jpg?width=1077,height=689.28 1077w, /images/nairn-beach.jpg?width=1500,height=960 1500w, /images/nairn-beach.jpg?width=2154,height=1378.56 2154w" sizes="100vw, (min-width: 37rem) 50vw, (min-width: 70rem) 33rem" alt="Me wearing a pride flag kilt, black shirt and smiling holding a champagne glass" width="4080" height="3072" loading="lazy">
  <img class="rounded row-span-2" src="/images/edinburgh-pride-2024.jpg?width=528" srcset="/images/edinburgh-pride-2024.jpg?width=400 400w, /images/edinburgh-pride-2024.jpg?width=600 600w, /images/edinburgh-pride-2024.jpg?width=800 800w, /images/edinburgh-pride-2024.jpg?width=1077 1077w, /images/edinburgh-pride-2024.jpg?width=1500 1500w, /images/edinburgh-pride-2024.jpg?width=2154 2154w" sizes="100vw, (min-width: 37rem) 50vw, (min-width: 70rem) 33rem" alt="Me wearing a pride flag kilt, black shirt and smiling holding a champagne glass" width="3072" height="4080" loading="lazy">
  <img class="rounded" src="/images/marseille.jpg?width=528,height=337.92" srcset="/images/marseille.jpg?width=400,height=256 400w, /images/marseille.jpg?width=600,height=384 600w, /images/marseille.jpg?width=800,height=512 800w, /images/marseille.jpg?width=1077,height=689.28 1077w, /images/marseille.jpg?width=1500,height=960 1500w, /images/marseille.jpg?width=2154,height=1378.56 2154w" sizes="100vw, (min-width: 37rem) 50vw, (min-width: 70rem) 33rem" alt="Me wearing a pride flag kilt, black shirt and smiling holding a champagne glass" width="4080" height="3072" loading="lazy">
</div>

      <hr>
      <p>If you have any comments or feedback on this article, let me know! I'd love to hear your thoughts, go ahead and <a href="mailto:alistair@accudio.com">send me an email</a> at alistair@accudio.com or <a href="https://front-end.social/@accudio" rel="noopener noreferrer">contact me on Mastodon</a>.</p>
      <p>The post <a href="https://www.alistairshepherd.uk/writing/spring-summer-2024/">Spring and Summer 2024 — Life, links and lmedia</a> appeared first on <a href="https://alistairshepherd.uk">alistairshepherd.uk</a>.</p>
    ]]>
      </content>
    </entry>
  
    
    <entry>
      <title>My anti-overlay client letter</title>
      <link href="https://www.alistairshepherd.uk/writing/accessibility-overlays-email/"/>
      <updated>2024-11-06T00:00:00Z</updated>
      <id>https://www.alistairshepherd.uk/writing/accessibility-overlays-email/</id>
      <content type="html">
        <![CDATA[
      <p>In agency and freelance web development, relatively often I'm asked by clients what I think about accessibility overlays, or asked to implement them.</p>
<p>I <strong>despise</strong> accessibility overlays, and get really quite upset when they're mentioned in a serious context. Unfortunately, getting increasingly upset and angry doesn't do a very good job of informing clients about why they're often a bad idea and why they shouldn't be trusted.</p>
<p>In many cases I can explain fairly succinctly the issues with overlays and that’s where the conversation finishes. Sometimes though the conversation escalates and my initial advice is ignored. For those circumstances I’ve put together a ‘serious letter’ that is designed to put all our cards on the table and have a serious tone.</p>
<p>It’s a carefully considered, standardised letter to explain why we don't like overlays, list some of the facts, and to try to convince a client that they're rarely a good option for them. I'm publishing this as it may be helpful to others in the same situation! Read to the end if you want a bit more context/explanation.</p>
<hr>
<p>Hi <code>[client name]</code>,</p>
<p>We strongly recommend against trying to solve accessibility issues on websites using 'accessibility overlays'. This will be a slightly long email to explain why that's the case and to make you aware of the risks. If you have any questions or want to discuss further then let me know and I'd be happy to do so.</p>
<p>An 'accessibility overlay' is a product that typically provides JavaScript code that you install on your website with the goal of improving accessibility. These services can provide various functionality, including the promise of automatic fixes and widgets with user controls. In rare cases they can be helpful tools, but evidence shows that they are generally not sufficient to substantially improve website accessibility, and cannot replace making sure a website is built accessibly. Web accessibility is a complex field and at the moment there are no tools or services that can reliably fix all issues. Believe me, I'd love it if there was! Evidence shows that even with cutting-edge technology (including AI) only about 30% of accessibility issues can be found without human intervention.</p>
<p>Some overlay companies often claim &quot;ADA compliance&quot; or &quot;complete accessibility&quot;, making false claims that their tools can fix website accessibility completely but that is not the case. They are often marketed as a quick-fix solution to avoid legal risk, or to avoid spending time on website accessibility.</p>
<p>From a <strong>user accessibility perspective</strong>, many of the representations of 'fixing' problems some of these solutions claim to do are misconstrued, false or greatly exaggerated. As someone who works in the accessibility field I know more disabled users who intentionally block these solutions than rely on them, thanks to many cases of them causing more harm than good.</p>
<p>From a <strong>business and reputational perspective</strong>, many of the big accessibility overlay companies are considered bad actors within the accessibility industry. Many companies have established history that can include untrue claims, falsifying evidence, paying for praise and reviews, and for silencing critics via 'SLAPP' lawsuits. Some of the largest companies in the industry have or continue to engage in some of these.</p>
<p>The <strong>legal protection promises</strong> are why many people look to and consider accessibility overlays. I would first clarify that I am not a lawyer, do not have legal experience, and this is not legal advice. Accessibility laws exist to provide equal access and to prevent companies from discriminating — intentionally or not — against those with disabilities. Examples include the Americans with Disabilities Act, and the European Accessibility Act. In many countries there is accessibility legislation that applies to websites, and requires websites to meet a certain level of accessibility.</p>
<p>Particularly in the USA, people can sue website owners for not providing equal access, which the ADA provides precedence for. This has become more frequent in the previous few years. Whilst many lawsuits are from individuals genuinely seeking equal access, you will find these don't come 'out of the blue' and people will seek assistance and make complaints first. <br>
Extremely unfortunately, there is also a large industry of 'malicious' accessibility lawsuits. These come from law firms targeting small companies in bad faith to exploit accessibility legislation for their gain. The purpose of these malicious accessibility lawsuits is to find anything that can be described as an accessibility issue in your website, regardless of how valid that is. Typically these law firms offer to settle, almost immediately, for a substantial fee.</p>
<p>As accessibility overlays cannot detect and fix all accessibility issues, they cannot prevent the circumstances that fuel these lawsuits. This means that <strong>regardless of your overlay, malicious law firms can definitely still find issues to sue you for, including violations caused by the overlay itself</strong>. I have seen no real evidence that an accessibility overlay genuinely reduces the likelihood of an accessibility lawsuit. In fact, some early research seems to suggest a potential increase in accessibility lawsuits following installation of a handful of particular accessibility overlays as their weaknesses and limitations are well-understood and easily identified. You will also find that several overlay companies have been cited in lawsuits as a reason for <em>worse</em> accessibility, and even caused issues that weren't present before installation.</p>
<p>Ultimately it's up to you, but our suggestion is that accessibility is always best solved at the source, and audited by a full manual accessibility test. If accessibility is considered during design, development and content input then not only are you providing the best experience to your users, but also your website is in the best state it can be for accessibility. This reduces legal risk, your reliance on third-parties and in many cases cost. By providing an &quot;Accessibility Statement&quot; page you can also allow people to contact and inform you of issues without escalation. The <a href="https://www.w3.org/WAI/planning/statements/generator/">W3C provides an Accessibility Statement generation tool</a>.</p>
<p>Regardless of what you opt for, we will continue to implement accessibility in our designs, development, and content input as is our best practice. We would encourage you to also consider accessibility when inserting content, and we are more than happy to support you and your team with that. We can offer training, accessibility testing, and recommend trustworthy third-parties who can provide audits or legal support.</p>
<p>We can add the code for an overlay to the website if you provide it and instruct us to do so, but this would be against our advice. If you choose to do so, we will discuss next steps including steps to ensure legal responsibility is clear.</p>
<p>Here are some resources that may be of interest to you regarding this, additionally I'd be happy to answer any questions you may have.</p>
<ul>
<li><em>Include relevant sources here. More details and examples below.</em></li>
</ul>
<p>Let me know how you'd like to proceed, if you have any questions, or if we can provide any support in the accessibility of your website.</p>
<p>Best regards</p>
<hr>
<p>The first thing I want to say is that you and I are not the intended recipient of this email. It's unemotional, business-y, overly focused on legal risk and business practice, and not very focused on the difficulties that disabled users actually face using the web.</p>
<p>Most of the clients who speak to me about accessibility overlays are founders of mostly ecommerce startups in the US so the tone and wording of this email is focused on what works for them. Before this point there's likely been conversation and discussion that is a lot more casual and focused on the benefits of accessibility. But when I'm sending this email, I want to get serious and convince them against the route they're thinking of, appealing to the business and legal side to get the message really heard.</p>
<p>I've found that for most of the clients I’ve worked with, explaining overlays in unemotional detail, justification, and evidence has the best success. That's also why there's a lot more of a focus on the legal side than I would like. You shouldn't be using these things because of the harm they cause, but when their legal counsel or other company founders say they need this, I find going for this kind of email works much more effectively.</p>
<p>It is very long, and that’s something I’ve struggled a little with. I would prefer to be more succinct, but for me I’ve had the most success putting everything on the table in clear detail, despite the length. The focus is to get all the information needed in there and then the ball’s in the court of our client.</p>
<p>I think most of the email is pretty self-explanatory, but there are a few other things I've put attention into that I want to explicitly call out.</p>
<p>You'll notice that I don't refer to any specific overlay companies anywhere in this post. I do generally customise this email more, focusing on the track record of any companies that have been mentioned. For posting this publicly however, I want to be quite careful on what I say against any companies in particular. If any specific overlay companies have been mentioned to you, I would suggest doing some research and tailoring the email more.</p>
<p>Something I think is crucial is a list of sources at the end of the email. Whilst earlier drafts of this post included some of my go-to links, these all reference particular companies and to be quite honest, SLAPP tactics work. I have faced legal threats about my blog before, and I’m eager to avoid it again. That really sucks, but the key point is I’d recommend doing some research and finding articles that are relevant. I’ve read some interesting articles from big publications like The New York Times, Forbes and Wired that you may consider. Adrian Roselli also has some very detailed articles about potentially poor practices and analysis about particular overlay companies. I also often include links to ongoing or past lawsuits — often class action — to show this is a very active topic.</p>
<p>Anyway, if you got this far I hope this is useful to you! Keep up the good fight against Accessibility Overlays, and make sure to add your signature to the <a href="https://overlayfactsheet.com/">Overlay Fact Sheet</a> if you haven't already!</p>
<p>Thank you so much to <a href="https://adrianroselli.com">Adrian Roselli</a> who kindly took some time to review this article and give me some of his thoughts and advice. I cannot thank him enough for helping me with it.</p>

      <hr>
      <p>If you have any comments or feedback on this article, let me know! I'd love to hear your thoughts, go ahead and <a href="mailto:alistair@accudio.com">send me an email</a> at alistair@accudio.com or <a href="https://front-end.social/@accudio" rel="noopener noreferrer">contact me on Mastodon</a>.</p>
      <p>The post <a href="https://www.alistairshepherd.uk/writing/accessibility-overlays-email/">My anti-overlay client letter</a> appeared first on <a href="https://alistairshepherd.uk">alistairshepherd.uk</a>.</p>
    ]]>
      </content>
    </entry>
  
    
    <entry>
      <title>HTMHell Advent Calendar — Preloading fonts for web performance with link rel=”preload”</title>
      <link href="https://www.alistairshepherd.uk/writing/htmhell-advent-calendar-24/"/>
      <updated>2024-12-25T00:00:00Z</updated>
      <id>https://www.alistairshepherd.uk/writing/htmhell-advent-calendar-24/</id>
      <content type="html">
        <![CDATA[
      <p>A post on the the HTMHell Advent Calendar about how to use rel=&quot;preload&quot; to improve font-loading performance!</p>
<p><a href="https://htmhell.dev/adventcalendar/2024/25/">Available in full on the HTMHell Advent Calendar</a></p>

      <hr>
      <p>If you have any comments or feedback on this article, let me know! I'd love to hear your thoughts, go ahead and <a href="mailto:alistair@accudio.com">send me an email</a> at alistair@accudio.com or <a href="https://front-end.social/@accudio" rel="noopener noreferrer">contact me on Mastodon</a>.</p>
      <p>The post <a href="https://www.alistairshepherd.uk/writing/htmhell-advent-calendar-24/">HTMHell Advent Calendar — Preloading fonts for web performance with link rel=”preload”</a> appeared first on <a href="https://alistairshepherd.uk">alistairshepherd.uk</a>.</p>
    ]]>
      </content>
    </entry>
  
    
    <entry>
      <title>The Blog Questions Challenge</title>
      <link href="https://www.alistairshepherd.uk/writing/blog-questions-challenge/"/>
      <updated>2025-03-12T00:00:00Z</updated>
      <id>https://www.alistairshepherd.uk/writing/blog-questions-challenge/</id>
      <content type="html">
        <![CDATA[
      <p>Some answers to questions about my experience blogging here and in the past! Been seeing this elsewhere and found them great fun to read. I'm also writing this at a <a href="https://events.indieweb.org/2025/03/homebrew-website-club-edinburgh-writing-hour-wdGHAkmO8qtA">Homebrew Website Club Writing Hour</a> in Edinburgh, which has been great fun!</p>
<h2>Why did you start blogging in the first place?</h2>
<p>Work mostly! In 2017 I dropped out of university and was attempting freelance web development as a career, trying to make something out of the hobby I had for a few years.</p>
<p>My first few clients came pretty easily through friends, former colleagues and neighbours but after a few months the leads started to dry up.</p>
<p>To try and get more work and get my name out there I wrote several blog posts targeted to small business owners. The goal was to convince them to upgrade/improve their website and introduce them to some of the concepts.</p>
<p>Here are some of those early blog titles:</p>
<ul>
<li>The Web in 2017 and what it means for your Website and Social Media!</li>
<li>Introducing: Responsive and Mobile-Focused Web Design</li>
<li>Introducing: Content Management Systems</li>
</ul>
<p>And they worked! Friends, family, and clients shared them mostly on Facebook and got me a handful more projects, including the big break that really drove my early career.</p>
<h2>What platform are you using to manage your blog and why did you choose it?</h2>
<p>Currently my website is built with <a href="https://www.11ty.dev">11ty</a>, a wonderful static site generator based on node.js. Reasons why I chose it:</p>
<ul>
<li>It's really simple</li>
<li>Generates static HTML — easy and cheap to host and secure</li>
<li>I'm in full control over the page content</li>
<li>Blog content can be in markdown — super portable</li>
</ul>
<h2>Have you blogged on other platforms before?</h2>
<p>My very first post was written on WordPress but never published on it. I'm a bit too extra sometimes so found that too mainstream for me.</p>
<p>I briefly used <a href="https://subrion.org">Subrion CMS</a> for my first few posts as I was trying that out for the rest of my site, but quickly realised it wasn't great for blogging.</p>
<p>I then used <a href="https://github.com/pagekit/pagekit">PageKit CMS</a>, a PHP+Vue-based CMS that hasn't been touched in about 9 years. No particular reason for this one in particular really, I just liked it and it was niche and a little weird.</p>
<p>From 2018 to 2020 I didn't have a blog, and then for a brief period I used <a href="https://dev.to/accudio">dev.to</a> before switching to my current stack in 2021. My blog posts are still cross-posted over on <a href="http://dev.to">dev.to</a>, hello if you're reading there!</p>
<h2>How do you write your posts? For example, in a local editing tool, or in a panel/dashboard that's part of your blog?</h2>
<p>Always local, the tool itself varies though. This post right now I'm writing in <a href="https://anytype.io">AnyType</a> which I'm trying as a local note-taking tool, but I've also used <a href="https://ia.net/writer">iA writer</a>, word documents, or just writing markdown straight into my code editor.</p>
<h2>When do you feel most inspired to write?</h2>
<p>Currently Sunday mornings end up being my writing time. Sunday tends to be my chill day and my partner gets up a bit later so I do an hour or two of writing with a tea after breakfast.</p>
<p>Previously it has been Friday afternoons with my work 'Personal development time', but that's been taken up by other stuff more recently.</p>
<h2>Do you publish immediately after writing, or do you let it simmer a bit as a draft?</h2>
<p>Generally immediately. My partner is a great proofreader and editor so for some posts I may ask him to proof and give me some feedback, in which case it simmers for a few days. If I'm not  getting it proofed though it goes live immediately.</p>
<h2>What are you generally interested in writing about?</h2>
<p>Front-end web stuff mostly, but I've done and will do some non-web posts when something interests me enough.</p>
<h2>Who are you writing for?</h2>
<p>Hmmm that's a tricky one!</p>
<p>I want to say it's all for myself and often it is, to put down my thoughts somewhere permanent so I can reference them later.</p>
<p>I enjoy sharing my knowledge and experience with others — it may be helpful and I tend to assume what I know everyone else does so it helps me stay on top of that assumption.</p>
<p>There's definitely a little bit of attention seeking, wanting people to see and recognise me. Also in my work it just makes good sense for my career — many of the people I want to network with, work with and be employed by care about blog posts. It's a good selling point!</p>
<h2>What's your favourite blog post?</h2>
<p>My all time favourite is probably <a href="https://alistairshepherd.uk/writing/svg-generative-ridges/">SVG generative mountain ridge dividers,</a> as thanks to <a href="https://letorey.co.uk">Dave Letorey</a> from <a href="https://stateofthebrowser.com">State of the Browser</a> it opened up the world of Conference talks to me.</p>
<p>Other top contenders for various reasons:</p>
<ul>
<li><a href="https://alistairshepherd.uk/writing/selling-web-project/">Selling a small front-end web project — what I learned</a></li>
<li><a href="https://alistairshepherd.uk/writing/cma-mobile-browsers-remedies/">My response to UK CMA's potential remedies for mobile browser competition</a></li>
<li><a href="https://alistairshepherd.uk/writing/grapheneos/">GrapheneOS as my daily-driver mobile OS</a></li>
</ul>
<h2>Any future plans for your blog? Maybe a redesign, a move to another platform, or adding a new feature?</h2>
<p>I currently have a complete redesign and rebuild in very early design stages. It has been there for about 14 months so far though so slow progress!</p>
<p>I'm struggling a little with the design but once I have that nailed it'll be all new gimmicks, themes, and features!</p>
<p>Whether it's a new platform I'm not yet sure, I love 11ty and it's super simple but have been considering a CMS for easier writing and editing on the go. I'm not convinced if I'm willing to give up the portability of markdown for that, we'll see!</p>
<h2>Who's next?</h2>
<p>I've seen loads of people doing this and honestly have no idea who has and hasn't. If you're reading this though and haven't written one then I tag you! I did it based on <a href="https://localghost.dev/blog/the-blog-questions-challenge/">Sophie Koonin</a> saying the same thing at the end of hers.</p>

      <hr>
      <p>If you have any comments or feedback on this article, let me know! I'd love to hear your thoughts, go ahead and <a href="mailto:alistair@accudio.com">send me an email</a> at alistair@accudio.com or <a href="https://front-end.social/@accudio" rel="noopener noreferrer">contact me on Mastodon</a>.</p>
      <p>The post <a href="https://www.alistairshepherd.uk/writing/blog-questions-challenge/">The Blog Questions Challenge</a> appeared first on <a href="https://alistairshepherd.uk">alistairshepherd.uk</a>.</p>
    ]]>
      </content>
    </entry>
  
    
    <entry>
      <title>Device notifications via HTTP with ntfy</title>
      <link href="https://www.alistairshepherd.uk/writing/ntfy/"/>
      <updated>2025-05-13T00:00:00Z</updated>
      <id>https://www.alistairshepherd.uk/writing/ntfy/</id>
      <content type="html">
        <![CDATA[
      <p>I really like <a href="https://ntfy.sh">ntfy</a> — a service that allows you to easily send notifications to your phone with just a HTTP request. It's a handy little tool to be able to do mobile notifications to yourself really easily with web standards.</p>
<p>I'm fairly technical, and make a lot of my own little things. Small apps that do what I want, integrations and such. Because I work with web, they're all little web apps. Sometimes what I need is a way to send a notification to my phone. I don't need a PWA or native app or anything like that, just a notification with some custom content, and I can tap it to go to a website.</p>
<p>ntfy is just that. On my phone I install the app and subscribe to a 'topic' (which is basically just a named event stream). Then I can send notifications to my device simply by making a web request to the ntfy service with that topic name. The topic could be secret — like a long secret key that no one else knows — for private notifications, or it could be public — a notification channel that anyone can subscribe to!</p>
<h2>Sending a notification with NTFY</h2>
<p>For the purposes of this blog, let's call my topic &quot;alistair-blog-demo&quot;. I can send a notification to folk who are subscribed to that topic with just:</p>
<pre class="language-js"><code class="language-js"><span class="token keyword">await</span> <span class="token function">fetch</span><span class="token punctuation">(</span><span class="token string">'https://ntfy.sh/alistair-blog-demo'</span><span class="token punctuation">,</span> <span class="token punctuation">{</span><br>    <span class="token literal-property property">method</span><span class="token operator">:</span> <span class="token string">'POST'</span><span class="token punctuation">,</span><br>    <span class="token literal-property property">body</span><span class="token operator">:</span> <span class="token string">'Hello! This is a test notification'</span><br><span class="token punctuation">}</span><span class="token punctuation">)</span><br></code></pre>
<p>And get the following notification a few seconds later!</p>
<img class="measure-long" src="/images/ntfy-1.png?width=820" srcset="/images/ntfy-1.png?width=400 400w, /images/ntfy-1.png?width=600 600w, /images/ntfy-1.png?width=800 800w, /images/ntfy-1.png?width=1077 1077w, /images/ntfy-1.png?width=1500 1500w, /images/ntfy-1.png?width=2154 2154w" sizes="(min-width: 52rem) 800px, calc(100vw - 2.66rem)" alt="Screenshot of android notification menu with a notification from ntfy. It has a title of ntfy.sh/alistair-blog-demo, and body of 'Hello! This is a test notification'" width="1080" height="1079" loading="lazy">
<p>You can get fancier with it with more parameters, all are <a href="https://docs.ntfy.sh/publish/">documented super well</a> including which platforms support what parameters.</p>
<pre class="language-js"><code class="language-js"><span class="token keyword">await</span> <span class="token function">fetch</span><span class="token punctuation">(</span><span class="token string">'https://ntfy.sh'</span><span class="token punctuation">,</span> <span class="token punctuation">{</span><br>  <span class="token literal-property property">method</span><span class="token operator">:</span> <span class="token string">'POST'</span><span class="token punctuation">,</span><br>  <span class="token literal-property property">body</span><span class="token operator">:</span> <span class="token constant">JSON</span><span class="token punctuation">.</span><span class="token function">stringify</span><span class="token punctuation">(</span><span class="token punctuation">{</span><br>    <span class="token string-property property">"topic"</span><span class="token operator">:</span> <span class="token string">"alistair-blog-demo"</span><span class="token punctuation">,</span><br>    <span class="token string-property property">"message"</span><span class="token operator">:</span> <span class="token string">"Alistair has posted a new blog post: Device notifications via HTTP with ntfy"</span><span class="token punctuation">,</span><br>    <span class="token string-property property">"title"</span><span class="token operator">:</span> <span class="token string">"New blog post from Alistair"</span><span class="token punctuation">,</span><br>    <span class="token string-property property">"tags"</span><span class="token operator">:</span> <span class="token punctuation">[</span><span class="token string">"rocket"</span><span class="token punctuation">]</span><span class="token punctuation">,</span><br>    <span class="token string-property property">"click"</span><span class="token operator">:</span> <span class="token string">"https://alistairshepherd.uk/writing/ntfy/"</span><br>  <span class="token punctuation">}</span><span class="token punctuation">)</span><br><span class="token punctuation">}</span><span class="token punctuation">)</span><br></code></pre>
<img class="measure-long" src="/images/ntfy-2.png?width=820" srcset="/images/ntfy-2.png?width=400 400w, /images/ntfy-2.png?width=600 600w, /images/ntfy-2.png?width=800 800w, /images/ntfy-2.png?width=1077 1077w, /images/ntfy-2.png?width=1500 1500w, /images/ntfy-2.png?width=2154 2154w" sizes="(min-width: 52rem) 800px, calc(100vw - 2.66rem)" alt="Screenshot of android notification menu with a notification from ntfy. It has the content from the previous code segment" width="1080" height="1132" loading="lazy">
<h2>BeReal NTFY</h2>
<p>I've built a handful of private little apps that notify me using ntfy, one public topic I've set up is ntfy notifications for the app BeReal, on <a href="https://bereal-ntfy.netlify.app">BeReal ntfy</a> (I know, imaginative).</p>
<p>BeReal is an app that gets everyone to take a photo of themselves and what they're doing once at the same time every day (timezone dependent). You can then see what your friends are up to at that time!</p>
<p>For an app that has like three functions — notification, photo, view — it does the notification bit miraculously badly. It's pretty inconsistent, and when I have it on a separate GrapheneOS profile, I wanted to build my own notification system.</p>
<p>ntyf made that easy and it look like 30 lines of code — receive a webhook when it's BeReal time, send a POST request with ntfy.</p>
<h2>Limitations and thoughts</h2>
<p>ntfy is pretty basic, despite how handy it is! It's definitely the type of tool for a little cheap fun tool, rather than a proper app but honestly that's exactly what I want. It gives me more agency with my devices and more I can do without needing to learn new technology I don't need to or spend a lot of time on it.</p>
<p>The main thing to keep in mind is that ntfy topics are public. I can send notifications to that URL but so can anyone else! My secret topic for private is all very well until someone finds it and can subscribe to my stuff without me knowing. You can pay to reserve a topic to prevent people posting but it would be visible to anyone so don't use it for anything super secret. My &quot;server under increased load&quot; notification isn't a big deal if someone else sees it.</p>
<p>Anyway, wanted to share a handy little tool that others may find useful!</p>

      <hr>
      <p>If you have any comments or feedback on this article, let me know! I'd love to hear your thoughts, go ahead and <a href="mailto:alistair@accudio.com">send me an email</a> at alistair@accudio.com or <a href="https://front-end.social/@accudio" rel="noopener noreferrer">contact me on Mastodon</a>.</p>
      <p>The post <a href="https://www.alistairshepherd.uk/writing/ntfy/">Device notifications via HTTP with ntfy</a> appeared first on <a href="https://alistairshepherd.uk">alistairshepherd.uk</a>.</p>
    ]]>
      </content>
    </entry>
  
    
    <entry>
      <title>I displayed an open graph image and had to pay how much?!</title>
      <link href="https://www.alistairshepherd.uk/writing/open-graph-licensing/"/>
      <updated>2025-07-22T00:00:00Z</updated>
      <id>https://www.alistairshepherd.uk/writing/open-graph-licensing/</id>
      <content type="html">
        <![CDATA[
      <p>A media company demanded a license fee for an Open Graph image used on my twitter archive. I gave in and paid it, but what does that mean for open graph images and copyright?</p>
<p>In April 2025, I received an email from an image licensing company (hereby &quot;licensor&quot;) regarding an image used on my twitter archive. That image was owned by them, but used as the Open Graph image for a news article. They demanded I purchase a license or face consequences for the infringement of their copyright.</p>
<p>I ended up purchasing the license to make it go away. Although I'm unconvinced I was at fault and it's certainly not a standard copyright case, it was not clear enough for me to risk those consequences. It really doesn't feel right, makes me wary of the Open Graph standard, angry about copyright on the web, and generally pissed off.</p>
<p>It also raises questions about the potential risks of displaying open graph images on the web.<br>
That's the short version, the TLDR. What will follow is me going a bit more in-depth with some of the specific communication and my thoughts.</p>
<h2>Short disclaimer</h2>
<p>What I need to make clear from the beginning though is I am not a lawyer. Very far from it, so if you're in this same situation (I feel for you) please just take this as my experience and do your own research and find what works for you. I paid the fee so I also don't know how this would have gone in a court.</p>
<p>I've removed anything identifying — including details about the image itself and company, and paraphrasing all communication. I don't want to get into any further trouble by identifying companies — I've already had the displeasure of that experience before.</p>
<h2>The rough context</h2>
<p>I left Twitter in November 2022, moving over to Mastodon as my main source of Social Media. At that point though I enjoyed my digital footprint and wanted to retain a record of my history on Twitter. I used <a href="https://github.com/tweetback/tweetback">Tweetback</a> to do this, it's an open-source tool written by Zach Leatherman and maintained by some good folk who were in a similar situation to me. You feed it your twitter archive export and it generates a static website showing off your tweets, including some extra functionality like frequency graphs and such. It's a really cool tool for that purpose!</p>
<p>More specifically, it generates listing pages and a page per tweet, with the same URL format as Twitter so you can easily turn one into the other. It acts independently of twitter, downloading the images you've uploaded, and hosting the content directly. Like most social media sites and Twitter itself, when you include an external URL it displays the Open Graph image for that URL.</p>
<details>
  <summary>What is Open Graph?</summary>
  <p>If you're not familiar, Open Graph is a 'protocol' created by Facebook for websites to provide metadata to 'enrich' the display of links on Facebook. Websites can provide details like a title, author, and image that could be presented alongside the link with a <code>&lt;meta property="og:image" ...&gt;</code> element. Tweetback uses the 11ty <a href="https://www.11ty.dev/docs/services/opengraph/">OpenGraph Image API</a> to fetch these images in a privacy-respecting and optimised way and displays them on tweet pages.</p>
</details>
<p>I published my Tweetback archive online with a robots value of 'noindex' so it wouldn't be indexed by search engines, and basically completely forgot about it. I'd reference it every few months maybe and occasionally added new functionality, but as far as I can tell based on traffic info basically no one ever looked.</p>
<h2>The initial email</h2>
<p>Okay, that's all the context you need to understand where we're at come April 2025. That's when I received an email in my inbox from an image licensing company which went something like this:</p>
<blockquote>
<p>We have found one of the images from our collection on your website. We own the copyright to images taken by our photographers and add them to a web crawler to scan the web.</p>
<p>We can't find a license from your organisation, so are checking you have a valid license. Please send us a copy.</p>
<p>If you don't have one, this would be a copyright infringement and you must purchase a license for £800 to cover your use in the past. [link to website to manage+pay]</p>
<p>You must also remove the image immediately until you have purchased but that would not be sufficient, you must pay for your previous use also.</p>
<p>[specific details including screenshots, links, etc]</p>
</blockquote>
<p>(once again, heavily paraphrased to just give the right impression)</p>
<p>Obviously whilst reading this I shat myself. The price they wanted was pretty damn high (approx £800/€900/$1000), and the language strong. It wasn't mailicious nor did it make direct legal threats — which I do appreciate — but certainly setting a clear impression of &quot;don't mess with us, we're serious like&quot;.</p>
<h2>The detailed context</h2>
<p>I've explained enough about the setup, so let's go into this specific use and this page.</p>
<p>In 2021 I made a tweet about UK politics, linking to an article from a national newspaper website. Not necessarily one of the bigger ones, but certainly one that most brits would know. It was a classic tweet sharing an article I enjoyed, about how a politician I didn't like was doing something I really didn't like.</p>
<p>That article included an Open Graph image, a photo of said politician speaking. On the twitter archive the page included my tweet — a short statement that summarised the article and my thoughts, and a link to the article. As Twitter would directly, Tweetback displayed the open graph image for that URL below.</p>
<p>That's the image that the licensor was referring to. The newspaper included it in a protocol designed to allow for the showing and sharing of images alongside their links, and then separately the licensor found my use of said image and demanded a license fee.</p>
<h3 id="hosting" tabindex="-1">On hosting and &quot;use&quot;</h3>
<p>One detail I missed from the initial version of this post was the hosting situation, which was a big omission! It was in an earlier draft but I must have cut it and not realised. I don't know how much of an impact where the image is hosted legally, but it certainly makes for a different perception and understanding as many people in my mentions questioned it.</p>
<p>I did not download and re-host any open graph images, including this one. Tweetback uses the <a href="https://www.11ty.dev/docs/services/opengraph/">11ty OpenGraph Service</a>, which is a third-party service run by the wonderful folk at 11ty. It accepts a URL and optional width and format, and will request the open graph image for that URL, cache it, and convert it to the specified width and format.</p>
<p>So just to make that very clear, I did not host the image myself and instead relied on a third-party service that requested it for me. That works in a similar way to other social media sites who use open graph — you wouldn't want every client requesting it direct from the server, that's a nightmare in terms of traffic and presents privacy issues too. So instead you have a server that caches it for several days.</p>
<p>That said, my research (more on that in a mo) suggests it may not matter from a legal standpoint. Copyright legislation doesn't make any stipulations about hosting — it's about &quot;use&quot; of an image and that's what the email I received focused on also. This makes sense, if they were addressing hosting then they would have gone to 11ty and not to me. As you'll see below, I asked a follow-up question about hosting, to which they suggested it was the &quot;use&quot; of the image on my site that was the infringement. That would be the same had I hotlinked directly to the newspaper.</p>
<p>There are also countless copyright infringement cases where people have 'hotlinked' images hosted on another site, which is functionally equivalent to linking directly to the paper's copy of the image pulled from open graph. So that precedent makes me dubious that linking directly to the OG URL would have been legally different.</p>
<p>Whether that difference would change the actions of the licensor is a different question entirely however!</p>
<h2>Immediately actions and research</h2>
<p>I'd been told if I paid the fee within 7 days, I would get a 10% discount so I had a week to decide if I was going to pay up or not. Waiting wasn't a good option, the price was high enough that the 10% off was significant enough to me if I decided I needed to pay.</p>
<p>So I researched copyright law, tried to find any historic precedent, and asked for help on social media. Thank you so much to everyone on Mastodon and Bluesky who lent me your thoughts or pointed me in helpful directions!</p>
<p>I also confirmed the copyright holder was who they claimed to be and that they genuinely held the license to the image. Regardless of how &quot;good faith&quot; this was, they definitely had the rights to the image!</p>
<p>The key thing I kept coming back to was that it's not like I 'took' this image, it was provided by a newspaper that had presumably properly licensed it and intentionally declared in their markup that this image could be used alongside that link. It's not like there is another use of the open graph protocol, that is it's purpose. But if there's a case around open graph then that's with the newspaper, not with the licensor, which would complicate things.</p>
<p>Looking through legal guidance it became very clear to me that there was not an easy answer here. The justification and explanation used in the email I received seemed correct, and a simple reading of the laws did not immediately identify an exception that my use would come under.</p>
<p>In the UK were this to go to court I understand it would be a small claims court where I would expect to be able to represent myself so I was not immediately looking for a lawyer like you may need elsewhere.</p>
<p>Based on the reading I had done, advice I was given from people in similar situations and my explanation of how open graph worked, I had a short further conversation with the the licensor. In summary:</p>
<blockquote>
<p>Alistair: explains open graph, how I wasn't hosting the image myself, how the site is not public and how I would not have purchased the license had I knew so potential losses are little to none.</p>
<p>Licensor: it's our copyright and we choose to impose a license fee for use. You used it on your site, therefore you owe a fee. We don't care about hosting, we see the image on your site so you need a license. Feel free to raise the open graph discussion with the website you sourced the image.</p>
<p>Alistair: I've found your subsidiary licenses the exact image for £20, I'd happily pay that. That's the price I would have paid if I had intended to license it.</p>
<p>Licensor: that's a different company, they can choose their prices and we choose ours. Either pay the fee we've asked for or not.</p>
</blockquote>
<h2>What did I do?</h2>
<p>As you'll know if you read the short version, I decided to pay up within the 7 days to at least get that 10% discount.</p>
<p>I believe fairly firmly that had they taken me to court over it it would have been favourable to me — at least more favourable than paying the full license fee. Considering many details — a subsidiary charged much less for the same image, I wouldn't have paid for the license in the first place, the estimated viewership of 0 people, the use of the Open Graph protocol — and looking at previous image copyright cases I was relatively confident of my side of this.</p>
<p>But ultimately, the easiest and cheapest option for me was still to pay. The chance that it was taken further and the potential cost in terms of money, time, energy from me was too high, higher than the license fee — even if I didn't feel it was justified. So I paid the fee and moved on.</p>
<h2>Open Graph images and copyright</h2>
<p>Okay so let's simplify and recap my experience:</p>
<ul>
<li>An organisation licenses an image from an image licensing company</li>
<li>They add it to the open graph markup of a webpage</li>
<li>Another webpage uses the open graph standard to display said image</li>
<li>Image licensing company then demands a high licensing fee</li>
</ul>
<p>Frankly, this is so fucked up. But I couldn't find a scrap of information anywhere on the web about the open graph protocol and copyright and the legal picture seems to suggest that's fine. It seems mad to me that there's no existing writing on this — open graph has been around for years and was created by Facebook, but it feels like I'm the first person to run into this as a concern?</p>
<p>I think it raises some serious questions about the risks of using open graph images on the web as an individual or small 'publisher'. According to the licensor, before displaying it I had an obligation to check with the newspaper the license used for the image and obtain one for myself.</p>
<p>This undermines the entire point of the open graph protocol (at least for images). If you have to manually review every image that you include then what's the point in it being a machine protocol?</p>
<p>Obviously, I didn't test this by letting it go to court. As previously mentioned I felt a fairly strong case for myself, but by paying up I've continued to let this be a question instead of being answered. If you're a lawyer who has an answer, or encounter a similar situation and want to stand your ground, please let me know. I'd be really interested to hear anything more solid from people who know more than I.</p>
<p>For me, I am no longer confident in open graph images being trustworthy, and I would not display them on any of my own sites. There is no circumstance where including open graph images is worth the cost I felt forced to pay in this instance.</p>
<h2>Who do I blame?</h2>
<p>This was an interesting question a friend asked me about this situation, and made me realise it's more fucked up than I thought. Here are my thoughts about where I would put the responsibility:</p>
<h3>Me</h3>
<p>Sure I'm a little biased, but nope. I used an extremely common web standard the way it was intended.</p>
<h3>The photographer</h3>
<p>Definitely not, they absolutely should have control over their photo and be able to sell it to a company to license on their behalf.</p>
<h3>The image licensing company</h3>
<p>I don't think I even blame the licensor, they owned the image and it's perfectly reasonable to search for misuse of your image and seek unpaid license fees. I do think they should have taken a much more nuanced approach and accounted for the circumstances however.</p>
<h3>The newspaper</h3>
<p>I would lay some blame here, they added an image without license for reuse to the open graph meta of their article. However there is no convention or rules that say you can't do that, and I imagine those who did so are completely unaware of the implications.</p>
<h3>Copyright laws</h3>
<p>Classic case of blaming the legislation? I don't think so, at least in the UK copyright law and guidance is surprisingly clear and easy to understand. It makes sense to me, and one technical edge case shouldn't need law changes.</p>
<h3>Facebook</h3>
<p>Maybe Facebook for not considering copyright within the open graph protocol? With this context I do think it was short-sighted to not account for or be explicit licensing in the protocol documentation. For example making it explicit that providing open graph images comes with a license for re-use in limited context of displaying the URL.</p>
<h2>Copyright on the web</h2>
<p>This whole ordeal has made me so fucking pissed off about copyright on the web in general.</p>
<p>At the same time that I'm having to pay far too much money on an image someone provided me implicitly for re-use, people like Nick Clegg are telling the world that AI companies need copyright exceptions. All my writing and images on my site and social media have been slurped up by AI companies and like fuck are they paying me a license fee for any of it.</p>
<p>This image was the open graph image of a front-page story for a major news publication. That article was shared hundreds to thousands of times on social media — do those companies also have to pay a license fee? Did Twitter, Facebook, Reddit etc have to pay the same fee I did if a single person shares that URL? I suspect not.</p>
<p>So it's a classic case of letting the big tech companies and their billionaires do whatever the fuck they want whilst extracting cash out of the people who can't protect themselves as well.</p>
<p>What a cheery note to wrap this up with!</p>
<h2>Re: the clickbait title and image</h2>
<p>Finally, I insincerely apologise for the clickbait-y title and accompanying open graph image of this post. When trying to come up with a serious title I came up with the clickbait-y version and knew it just had to be this way.</p>
<p>What's that? You're one of the delightful folk who subscribe via RSS and didn't see the open graph image? Well I wouldn't want to deprive you of the 5 valuable minutes I spent putting this together so here you go, as a little treat:</p>
<img class="measure-long rounded" src="/images/image-licensing-og.png?width=820" srcset="/images/image-licensing-og.png?width=400 400w, /images/image-licensing-og.png?width=600 600w, /images/image-licensing-og.png?width=800 800w, /images/image-licensing-og.png?width=1077 1077w, /images/image-licensing-og.png?width=1500 1500w, /images/image-licensing-og.png?width=2154 2154w" sizes="(min-width: 52rem) 800px, calc(100vw - 2.66rem)" alt="A YouTube-style clickbait thumbnail image. The title (in Lobster uwu) is 'They want *how much* for an Open Graph image?!!'. I'm looking alarmed with my mouth wide and my hands outstretched beside my head. The background is piles of british banknotes, and there's a blurred out money label at the bottom." width="2000" height="1125" loading="lazy">
<h2>Update 23/07/25</h2>
<p>Based on questions I got via social media yesterday evening, <a href="#hosting">I've added a section above about how the image was hosted</a> and asserting that I researched the company in question.</p>
<p>The hosting particularly was a big omission, I meant to include it but I must have accidentally removed it when editing or copying it onto my website. Oops!</p>

      <hr>
      <p>If you have any comments or feedback on this article, let me know! I'd love to hear your thoughts, go ahead and <a href="mailto:alistair@accudio.com">send me an email</a> at alistair@accudio.com or <a href="https://front-end.social/@accudio" rel="noopener noreferrer">contact me on Mastodon</a>.</p>
      <p>The post <a href="https://www.alistairshepherd.uk/writing/open-graph-licensing/">I displayed an open graph image and had to pay how much?!</a> appeared first on <a href="https://alistairshepherd.uk">alistairshepherd.uk</a>.</p>
    ]]>
      </content>
    </entry>
  
    
    <entry>
      <title>Simple website analytics with Goatcounter</title>
      <link href="https://www.alistairshepherd.uk/writing/simple-website-analytics-with-goatcounter/"/>
      <updated>2026-03-24T00:00:00Z</updated>
      <id>https://www.alistairshepherd.uk/writing/simple-website-analytics-with-goatcounter/</id>
      <content type="html">
        <![CDATA[
      <p>I'm not a big fan of analytics and tracking on the web — I use ad and tracking blockers in my browser, on my device, router, and built into the DNS service I use.</p>
<p>But having some idea of what pages are being visited, how people are using the website, and who is linking to me is helpful information! With that I can learn from what posts did well, put my attention for improvement where it matters, and understand how people use my site so I can make a future redesign and build better!</p>
<p>I have a few requirements for an analytics tool I'd be happy with:</p>
<ul>
<li>Privacy-preserving — collect only the information I use, and it shouldn't be used for any other purpose</li>
<li>Lightweight — no real impact on users</li>
<li>Simple — I should be able to quickly see the information I need without wading through menus</li>
<li>Cheap — I don't want to pay more than about £5 a month for it, it's not worth more than that to me</li>
<li>Self-hosted or a company I trust — either I control the hosting or trust who does to do the right thing</li>
</ul>
<h2>Analytics options</h2>
<p>I had a look into analytics options for a client a wee while ago and judged them against my requirements:</p>
<table>
<thead>
<tr>
<th style="text-align:left">Tool</th>
<th style="text-align:left">Privacy</th>
<th style="text-align:left">Lightweight</th>
<th style="text-align:left">Simple</th>
<th style="text-align:left">Cheap</th>
<th style="text-align:left">Hosting</th>
</tr>
</thead>
<tbody>
<tr>
<td style="text-align:left">Google Analytics</td>
<td style="text-align:left">❌</td>
<td style="text-align:left">❌</td>
<td style="text-align:left">❌</td>
<td style="text-align:left">✅</td>
<td style="text-align:left">❌</td>
</tr>
<tr>
<td style="text-align:left">Matomo</td>
<td style="text-align:left">✅</td>
<td style="text-align:left">❌</td>
<td style="text-align:left">❌</td>
<td style="text-align:left">✅</td>
<td style="text-align:left">✅</td>
</tr>
<tr>
<td style="text-align:left">Netlify Analytics</td>
<td style="text-align:left">✅</td>
<td style="text-align:left">✅</td>
<td style="text-align:left">✅</td>
<td style="text-align:left">❌</td>
<td style="text-align:left">✅</td>
</tr>
<tr>
<td style="text-align:left">Plausible</td>
<td style="text-align:left">✅</td>
<td style="text-align:left">✅</td>
<td style="text-align:left">✅</td>
<td style="text-align:left">❌</td>
<td style="text-align:left">✅</td>
</tr>
<tr>
<td style="text-align:left">Fathom</td>
<td style="text-align:left">✅</td>
<td style="text-align:left">✅</td>
<td style="text-align:left">✅</td>
<td style="text-align:left">❌</td>
<td style="text-align:left">✅</td>
</tr>
<tr>
<td style="text-align:left">Countly</td>
<td style="text-align:left">✅</td>
<td style="text-align:left">✅</td>
<td style="text-align:left">❌</td>
<td style="text-align:left">✅</td>
<td style="text-align:left">✅</td>
</tr>
<tr>
<td style="text-align:left">Umami</td>
<td style="text-align:left">✅</td>
<td style="text-align:left">✅</td>
<td style="text-align:left">✅</td>
<td style="text-align:left">✅</td>
<td style="text-align:left">✅</td>
</tr>
<tr>
<td style="text-align:left">Goatcounter</td>
<td style="text-align:left">✅</td>
<td style="text-align:left">✅</td>
<td style="text-align:left">✅</td>
<td style="text-align:left">✅</td>
<td style="text-align:left">✅</td>
</tr>
</tbody>
</table>
<p>Plausible and Fathom both seem like great options, but for a personal site, small hobby project, or small business they are just too expensive for me. Analytics are helpful and I don't want to diminish the value of the work these companies do, but I don't personally put enough value in analytics to be worth paying for these services.</p>
<p>I mostly host on Netlify at the moment who have a server-side analytics offering but it's expensive and would lock me in with them instead of being able to switch hosting so that's not an option for me.</p>
<p>Countly and Umami seemed pretty cool, both can be self-hosted and I gave them a try for a couple months. Countly I found a pain to maintain — upgrades and maintenance took a lot of work. Umami seemed really cool but a little rough and slightly buggy when I last looked at it.</p>
<p>I ended up settling on Goatcounter!</p>
<h2>Introduction to Goatcounter</h2>
<p>Goatcounter is a super simple analytics tool that is open-source and can be self-hosted. There's a public instance that you can use for free, supported by donations.</p>
<p>It collects the bare minimum of data (and allows you to customise that), is lightweight, you can export data, and even has no-JS options!</p>
<p>It is also really not a pretty interface! 😅</p>
<img class="rounded" src="/images/goatcounter-dashboard.png?width=1077" srcset="/images/goatcounter-dashboard.png?width=400 400w, /images/goatcounter-dashboard.png?width=600 600w, /images/goatcounter-dashboard.png?width=800 800w, /images/goatcounter-dashboard.png?width=1077 1077w, /images/goatcounter-dashboard.png?width=1500 1500w, /images/goatcounter-dashboard.png?width=2154 2154w" sizes="(min-width: 70rem) 67.34rem, calc(100vw - 2.66rem)" alt="Goatcounter dashboard, showing a primitive but functional interface with date controls, and a list of pages on this site with visitor graphs" width="2674" height="1584" loading="lazy">
<h2>Setting up and using</h2>
<p>But it doesn't need to be! It's just an analytics tool lol.</p>
<p>I haven't self-hosted but setting up via the public instance is easy — sign up via the website, it gives you a <code>script</code> to embed into your site, pop that in and you're good to go!</p>
<p>There are a lot of settings you can use to tailor exactly how it should behave. I've customised the data collection to disable &quot;Individual pageviews&quot;, &quot;Session&quot;, &quot;Region&quot;, and &quot;Language&quot; as it's not information I'm very interested in. You can even &quot;Show fewer numbers&quot; if you're just interested in general trends and don't want exact numbers!</p>
<p>In terms of using Goatcounter it's pretty straightforward. In the Dashboard interface you can filter the view by date or path from the top bar and then below you get widgets showing you the key information about:</p>
<ul>
<li>Page views</li>
<li>Referrers</li>
<li>Browser and OS versions</li>
<li>Countries</li>
<li>Screen sizes</li>
</ul>
<img class="rounded" src="/images/goatcounter-details.png?width=1077" srcset="/images/goatcounter-details.png?width=400 400w, /images/goatcounter-details.png?width=600 600w, /images/goatcounter-details.png?width=800 800w, /images/goatcounter-details.png?width=1077 1077w, /images/goatcounter-details.png?width=1500 1500w, /images/goatcounter-details.png?width=2154 2154w" sizes="(min-width: 70rem) 67.34rem, calc(100vw - 2.66rem)" alt="Goatcounter dashboard, with graphs representing top referrers, browsers, operating systems, countries and screen sizes" width="2674" height="1584" loading="lazy">
<p>The interface isn't pretty but it's very functional and actually does the job really well. I've considered doing a custom theme for clients as I think they'd shy away from Goatcounter purely due to the interface design, but not quite yet.</p>
<p>This has everything I want from my analytics in a single page I can check in the time it takes the Google Analytics dashboard to even load, let alone work out how to find the information I'm looking for!</p>
<h2>On privacy</h2>
<p>As mentioned at the beginning of this post I'm not a big fan of analytics in general, but there is a huge difference between the likes of Google Analytics and Goatcounter, Plausible and friends.</p>
<p>My line sits much closer to the Goatcounter end. I understand the value in website owners knowing who is visiting their website and being able to use that information to improve their website and writing. But that shouldn't come at the cost of tracking individual user behaviour and selling it to third-parties.</p>
<p>I also don't like the absolutist view that server-side logs are the only reasonable source for analytics. Relying on server-side logs only ties you to specific hosts and tools, limits you in hosting choice and performance, significantly complicates offline-first or distributed CDN architectures, and are typical much weaker at identifying bots.</p>
<p>I'm happy with a JS analytics provider that has a solid privacy policy, only collects and stores what's needed for aggregate reports and is open-source. Ideally I would self-host it also, and that's something I'll be looking into.</p>
<p>Finally, the ePrivacy directive and consent. I'm currently not including an opt-in consent dialog and instead focusing on making sure the analytics are as friendly and minimal as possible. I do believe that there is a strong case for ePrivacy requiring opt-in consent for even &quot;cookieless&quot; analytics (<a href="https://decoded.legal/blog/2024/08/web-analytics-tools-using-javascript-cookie-consent-and-the-uk/">see Neil Brown's fantastic post on decoded.legal</a>). For my work though I want to strike a balance between collecting some useful data, respecting privacy as much as possible, whilst also avoiding a consent banner.</p>
<p>If you wish to opt-out of analytics on this website you can enable <a href="https://www.eff.org/issues/do-not-track">Do Not Track</a>, the <a href="https://globalprivacycontrol.org">Global Privacy Control</a>, or <button class="btn-link" onclick="window.location.hash = '#toggle-goatcounter'; window.location.reload()">toggle analytics</button>.</p>
<h2>Final thoughts</h2>
<p>Goatcounter is a fantastically simple analytics tool that lets you get the key data you need to see how your website is doing. In terms of privacy it's pretty good, it's cheap, lightweight, and you can self-host it.</p>
<p>I'm really happy with it, and will consider self-hosting and using on most of my personal projects going forward!</p>

      <hr>
      <p>If you have any comments or feedback on this article, let me know! I'd love to hear your thoughts, go ahead and <a href="mailto:alistair@accudio.com">send me an email</a> at alistair@accudio.com or <a href="https://front-end.social/@accudio" rel="noopener noreferrer">contact me on Mastodon</a>.</p>
      <p>The post <a href="https://www.alistairshepherd.uk/writing/simple-website-analytics-with-goatcounter/">Simple website analytics with Goatcounter</a> appeared first on <a href="https://alistairshepherd.uk">alistairshepherd.uk</a>.</p>
    ]]>
      </content>
    </entry>
  
    
    <entry>
      <title>16 months with Loop Switch 2 earplugs</title>
      <link href="https://www.alistairshepherd.uk/writing/16-months-with-loop-switch-2-earplugs/"/>
      <updated>2026-03-25T00:00:00Z</updated>
      <id>https://www.alistairshepherd.uk/writing/16-months-with-loop-switch-2-earplugs/</id>
      <content type="html">
        <![CDATA[
      <p>If your advertising profile is similar to mine then there's a good chance you're familiar with Loop earplugs — it feels like every tech or tech-adjacent person I know has seen loads of ads from them online!</p>
<p>If you aren't familiar with <a href="https://www.loopearplugs.com">Loop</a>, they make a range of different earplugs for different circumstances, and present themselves as a fancy new startup that leans heavily on social media, influencer sponsorships, and media collaborations.</p>
<p>I went for it and bought a pair of their <a href="https://www.loopearplugs.com/products/switch">Switch 2 earplugs</a> in October 2024, and have been using them for almost a year and half now — time for a review and you can't claim my testing process is too fast! The question is do I like them? (as if the 18 months hasn't given it away!)</p>
<h2>Caveat — my hearing and ears</h2>
<p>Getting the caveat out the way — my ears and hearing is different from yours. There's the standard normal variation among folk, but I also have issues with my hearing — likely coming from auditory processing issues. Also for some reason my right ear canal is a weird and different shape from my left so that's fun! (it makes finding nice IEMs that fit an absolute <strong>nightmare</strong>)</p>
<p>My main issue with auditory processing is trouble differentiating foreground noise — someone's voice — from background music, other conversations, hubbub etc. I've had several hearing tests but nothing that comes up so it seems likely to be a mental disorder/issue. I mention it so you can use that as context with the rest of my thoughts.</p>
<p>So take everything here with a pinch of salt, what may work for me may not for you. this especially applies to the 'modes' of the Switch, which may make more sense for others than for me — but more on that in a bit.</p>
<h2>How I use earplugs, and what I used before</h2>
<p>I initially got earplugs for music concerts, when standing too near a speaker caused a month of ringing in my ears. <strong>PSA: if you go to live music you should have hearing protection!</strong></p>
<p>I now use them for quite a few different circumstances:</p>
<ul>
<li>Music gigs</li>
<li>Reducing traffic noise when walking around a city</li>
<li>Blocking transport noise — trains, planes, buses</li>
<li>Loud neighbours or street parties preventing me from sleeping</li>
<li>Focus while I work</li>
</ul>
<p>Previously I've used disposable foam earplugs — cheap (free at music venues in the UK), easy, sound like shit — and Alpine MusicSafe Pro — professional kit, recommended by experts. Unfortunately I didn't really get on with the MusicSafe Pro's — I used them for a while but they didn't fit my ears very well, and for music I felt like the energy and excitement was taken out by them.</p>
<h2>Enter the Loop Switch 2</h2>
<p>I actually bought the Loops originally based on their heavy advertising towards people who get sensory overload, and ads that showed people struggling to hear others in loud environments. They put in some Loops and suddenly the background noise was gone but they could still hear foreground noises easily. That sounds perfect for me!</p>
<p>Loop has a range of different earplugs with different noise levels for different circumstances. The Dream is for sleeping, Quiet is for focusing and travel, Experience for concerts, and Engage for conversation and socialising. The Switch is more expensive, and they claimed it combines Quiet, Experience and Engage behind a single switch that allows you to 'switch' between them — <em>while the earplug is still in your ear</em>.</p>
<p>I went for the Switch because I would use all three modes for my earplug use-cases above, and I can keep track of one pair of earplugs more easily than if I got several different pairs (I would definitely lose a pair within a month).</p>
<p>Ordering from their website, delivery, packaging etc all good, no major comments there. They feel premium and have a nice small carrying case.</p>
<p>In terms of fit, they're okay. They're a bit chunky and hard so you do definitely feel them if you wear them for a while (6+ hours for me), but most of the time I don't notice. You probably want to avoid other ear pressure for long periods, I found wearing them with my glasses and a mask around the back of my ears did make them quite sore after an hour or so.</p>
<p>They have replaceable tips like silicone earphones so you can get a good fit regardless of the size of your ear canal. Handy for me, my ears need different sizes!</p>
<h2>How do they sound?</h2>
<p>The big question is how they sound wearing them!</p>
<p>When I started using them initially, I was extremely disappointed. There were all those ads I mentioned and Loop talks big about the different modes for different circumstances. I had been led to expect the Engage mode would help me hear voices over background noise and that definitely wasn't happening. I was really not happy with them after a few days.</p>
<p>At some point when trying to work out my thoughts, I started thinking in terms of volume instead of modes. Forget about modes, with the Loop Switch you can choose how much volume the earplugs blocks by 'a bit', 'a decent amount', or 'quite a lot'. (I'll refer to them in amount of blocking, so higher is more blocking, lower is less)</p>
<p>When I adjusted those expectations I got along a lot better with them! I could put them in and instead of thinking about what circumstance I was in I could simply choose whether I wanted more or less blocking. At a gig that was particularly loud? I'll pop it up one. Want to be a little more aware of my environment when I'm out? Switch it down one so I can hear more.</p>
<p>The MusicSafe Pro has multiple inserts with different levels, but the ability to change without removing them is a game-changer. At gigs in particular I frequently find myself reducing for acoustic songs or my favourite tracks, increasing for heavier and louder songs.</p>
<p>In terms of the audio quality, they dull sound pretty equally across the spectrum and don't affect what you hear a huge amount other than volume. Listening to music you do lose detail in the top-end whilst bass gets through more easily, similar to other earplugs I've used before. I do much prefer them to the Alpine MusicSafe Pro however, and feel like live music still retains a good amount of energy and excitement despite the volume reduction. They are as expected <strong>much</strong> better than disposable foam earplugs.</p>
<p>When thinking in terms of volume reduction instead of the modes I really like them.</p>
<h2>'Engage' mode and conversation</h2>
<p>There is one exception, and that is with voices/conversation (the purpose of the &quot;Engage&quot; mode). As I've mentioned a couple times that's something Loop advertise a lot and I just don't get it.</p>
<p>I can only assume given how much they talk about it that it must work for some people, but it really doesn't for me. It turns a clear voice on top of background noise into a muffled and quieter voice on top of quieter background noise. For me, they are worse than useless when it comes to conversation.</p>
<p>So yeah, your mileage may vary but I'd be wary about buying Loops solely for the purpose of conversation, and extremely wary if you also have auditory processing or hearing issues. For &quot;Experience&quot; and &quot;Quiet&quot; sure, but I would be sceptical of their pitch for &quot;Engage&quot;.</p>
<h2>How I use my Loop Switch</h2>
<p>I carry them with me everywhere now and use them for:</p>
<ul>
<li>Music gigs — generally the lowest mode, sometimes pop it up to the middle if needed</li>
<li>Walking around in the city — the highest mode much of the time, lower when I need to be more aware</li>
<li>Transport — crack that one on highest, blocks pretty much all transport noise</li>
<li>Sleep — they are not comfortable when resting your head sideways on a pillow, so I don't do this often. But in a pinch they do work, and work well if you're sleeping on transport with a neck pillow. I'm considering getting a pair of the &quot;Dream&quot; earplugs for home.</li>
<li>Focus while I work — also on the highest most of the time when I want quiet whilst working</li>
</ul>
<p>After 9 months of daily use they're still in good condition and have no major differences from when I got them.</p>
<h2>Combined with bone conduction earphones</h2>
<p>This feels like a weird one — bone conduction earphones are designed to keep your ears free and you aware of your surroundings, whilst earplugs do the opposite!</p>
<p>That said, my primary earphones are bone conduction earphones, previously the <a href="https://alistairshepherd.uk/writing/aeropex-review/">Aftershockz Aeropex</a> and now the Shockz OpenRun Pro. And I don't want to have to own and carry a second pair of earphones when I want to focus on what I'm listening to.</p>
<p>It's not a perfect combo but it does work surprisingly well!</p>
<p>The main thing to note is the character of sound is changed quite significantly by inserting earplugs with bone conduction earphones. Essentially the earphones are tuned for open ears and by changing that you are blocking particularly the high frequencies from getting to your ears. Everything becomes a lot more bassy.</p>
<p>For speech like audiobooks and podcasts I don't notice it enough to find it a major issue.</p>
<p>With music it's really obvious. My music app has an equaliser that allows me to knock back the low frequencies and increase the high frequencies so by using that I have managed to match it up roughly to still be decent listening, but far from a great experience.</p>
<p>If you're really fussed about audio quality all the time, or you don't have access to an equaliser then this isn't for you. But if your phone/music app does have an equaliser and you're happy with okay sound quality then it works!</p>
<p>It definitely works better with purely bone conduction earphones like the Aeropex however, whilst earphones combining bone conduction and open speakers like the OpenRun Pro are much more muffled and perform worse so keep that in mind.</p>
<h2>Should I buy them?</h2>
<p>As mentioned, you should have hearing protection if you go to live music. Even if you don't use it, you will regret not having when you need it.</p>
<p>If I need earplugs anyway then I may as well make sure I have decent ones, and I think the Switch 2 is a really good pair of earplugs. Yes they're expensive, but for me the ease and versatility makes them easily worth it.</p>
<p>I would re-iterate that for me at least they're not at all useful for voices and conversation so if that's your primary use then don't get your hopes too high.</p>
<p>But on the other hand you have different ears from me — and thank goodness because I'm currently busy with mine.</p>

      <hr>
      <p>If you have any comments or feedback on this article, let me know! I'd love to hear your thoughts, go ahead and <a href="mailto:alistair@accudio.com">send me an email</a> at alistair@accudio.com or <a href="https://front-end.social/@accudio" rel="noopener noreferrer">contact me on Mastodon</a>.</p>
      <p>The post <a href="https://www.alistairshepherd.uk/writing/16-months-with-loop-switch-2-earplugs/">16 months with Loop Switch 2 earplugs</a> appeared first on <a href="https://alistairshepherd.uk">alistairshepherd.uk</a>.</p>
    ]]>
      </content>
    </entry>
  
</feed>
