Tom HazledineJS.SSGDeveloper. Podcaster. Nerd.2024-02-29T00:00:00Zhttps://tomhazledine.com/Tom Hazledinetom@tomhazledine.comRSS is Awesome2024-02-29T00:00:00Zhttps://tomhazledine.com/rss-is-awesome/<p>RSS is “having a moment” right now, and I’m 100% here for it. Well, maybe folks aren’t explicitly excited about RSS specifically, but an independent and open Web (a Weird Web, even?) is being talked about a lot. Burned by recent trends in <em>[looks around]</em> <strong>everything</strong>, we’ve realised that having some degree of control over the way our content is distributed is actually quite useful.</p><h2 id="we-almost-lost-it">We almost lost it</h2><p>When I wrote <a href="https://tomhazledine.com/adding-rss">a post about RSS in 2021</a> it felt so off-trend that I added a “yes, it’s still a thing” caveat right in the post’s title. And honestly, I wasn’t super confident that it <em>was</em> still “a thing”. So it’s massive relief to see that something I care a lot about is <em>growing</em> in relevance.</p><p>In short, blogs are back! And with the demise of algorithmic feeds (or at least their massively reduced reach) we’re all looking for reliable way to <em>find</em> all this content. And it turns out RSS was pretty much the best tool for the job all along.</p><h2 id="skin-in-the-game">Skin in the game</h2><p>A principle I’ve been trying to live by lately is that <strong>if I think something’s important I should be demonstrably involved</strong>. In some form or other I should have some skin in the game. To paraphrase a saying tied to musicians’ muscle memory: it’s not just enough to know something “in the head” - if I care about it I should know it “in the hand” too.</p><ul><li>I think podcasting is simply the best medium, <a href="https://aqoc.dev">so I’ve got a podcast</a></li><li>I think YouTube is an amazing outlet for the world’s creativity and knowledge, <a href="https://www.youtube.com/tomhazledine">so I’ve got a YouTube channel to share some things I’m super-nerdy about</a></li><li>I’m excited about the potential of Web Components (maybe we don’t all need React after all?!), so I’ve <a href="https://picobel.tomhazledine.com/web-component/">released one of my open-source tools as a Web Component</a></li></ul><p>My projects are not setting the world alight, but I’m <strong>involved</strong>. If podcasting or YouTube come up in conversation, I can talk from experience. I can legitimately say <a href="https://youtu.be/6xG4oFny2Pk?t=46">“I was there”</a>.</p><p>So how does that apply to RSS? Well, for starters I’ve been blogging in some form or other since 2008 so I’ve definitely put my fair share of RSS feeds out into the world. But that feels a bit <em>passive</em>. If I truly want to have some skin in the RSS game, I need to be contributing. I need to give something back, something that helps other folks (not just burdening them with something more of mine to read).</p><p>So I’ve built an RSS reader.</p><blockquote><p>Err, don’t we have those already?</p></blockquote><p>True, there are loads of RSS reader apps out there. And some of them are <a href="https://netnewswire.com/">really good</a>, too. But none are <em>exactly</em> what I’m looking for. And what’s the use of being a developer if you can’t build things you like?</p><h2 id="building-a-thing-is-the-best-way-to-learn-a-thing">Building a thing is the best way to learn a thing.</h2><p>In classic <em>“how hard can it be?”</em> tradition, I learned <strong>a lot</strong> while building my app.</p><ul><li>What we colloquially think of as RSS is actually a quite fragmented ecosystem with many flavours (RSS/Atom/JSON/etc.) that all have subtly different specifications. So writing a feed parser that can handle any feed a user might throw at it needs to account for the fact that YOU CANNOT ASSUME ANY “REQUIRED” FIELDS WILL BE PRESENT.</li><li>A properly useful feed reader consumes A LOT of data. I naively assumed that because all I’d be storing would be text content, the payloads would be small. But even with a limited set of test feeds, my local state soon hit tens of megabytes.</li><li>Everyone has a different set of priorities for what makes a “good” feed reader experience. Some people want notifications and statuses up the wazoo, some folks want the most minimal interface possible.</li></ul><p>And because RSS has been around for so long, there are a lot of “nice to have” features in pretty much every reader:</p><ul><li><strong>Auto-discovery of feed URLs.</strong> Being able to submit a plain ol’ URL for a site and have the reader-app find the feed URLs is table-stakes at this point.</li><li>Once you open the door to auto-discovery of feed URLs, you need your UI to be able to <strong>handle sites that have multiple different feeds</strong>.</li><li><strong>Resolving relative URLs within feed content</strong> (for links, images, etc.). Despite the best practice of making all URLs in feeds absolute, comparatively few feeds actually implement this (so dead links and missing images are super common when browsing a naively-parsed feed).</li><li><strong>Universal import and export via OPML file</strong>. OPML is a markup format that allows groups of feed URLs (or podcasts) to be exported and imported into pretty much any mainstream app that deals with RSS.</li></ul><h2 id="prioritising-the-features-i-care-most-about">Prioritising the features I care most about</h2><p>So building my RSS app was a useful exercise solely from a learning perspective, but I was also able to throw in some features purely because they’re what <em>I’m</em> interested in 😆</p><h3 id="an-almost-serverless-app">An (almost) serverless app.</h3><ul><li><strong>No accounts.</strong> I (currently) have very little interest in creating a <a href="https://en.wikipedia.org/wiki/Create,_read,_update_and_delete">CRUD</a> app or in dealing with all the user-management and server-side shenanigans that come with such an app, so from the get-go my USP for this project was “no accounts”. Users can just hit the homepage and get started using the reader functionality.</li><li><strong>Local storage.</strong> So if I’m not running a server and database, where does all the feed content go? The add needs some persistence of data, so naturally the browser’s “local storage” was the first thing I tried. Plain old <code>localStorage</code> turned out to be too small for all the data a feed reader needs to store, so I ended up using <code>localStorage</code>'s beefier buddy, <code>IndexedDB</code> (there's an entire database platform built right into the browser. Who knew?!). </li><li><strong>Static hosting.</strong> Having no backend means that deploying my reader app is miraculously simple, especially with static hosting platforms like Netlify.</li><li><strong>Edge functions.</strong> When I said the app was static and serverless, I lied. Thanks to (perfectly sensible and secure) CORS restrictions, client-side JavaScript can't just go around downloading content from arbitrary URLs willy nilly. And reading content from arbitrary URLs is pretty much the one key feature of an RSS reader. To get around this, the <em>"read the content of the URL"</em> part of my reader app lives on a Netlify "edge function". This is a simple Node.js function that accepts a feed URL, reads it, and returns the data to the client-side JS.</li></ul><h3 id="the-reading-experience-i-want">The reading experience I want.</h3><p>Another big win from a selfish standpoint is that by building my own feed reader app I get to craft whatever esoteric and bespoke reading experience I want.</p><p>I've written before about <a href="https://tomhazledine.com/html-doesnt-need-any-styling/">how little styling it takes to make vanilla HTML an excellent way to read content</a>, but I do like to put my own spin on things. Alongside basic typographic best-practices (like sensible line-lengths, line-heights, and font sizes) I appreciate a well-crafted <code><hr></code> element and a confidently opinionated <code><blockquote></code>.</p><p>And then there's the "reader app" specifics around feed management. Can the feeds be organised in groups? Can you drag-and-drop from one group to another? Can you fine tune your "unread" and "new" notifications?</p><p>Add to that my (perhaps unusual) preference for monospaced typefaces and loud accent colours in both light and dark themes, my ideal reading experience isn't something I can realistically expect anyone else to create for me.</p><h2 id="you-can-use-it-too">You can use it too</h2><p>So now I’ve sung my reader app’s praises, where can you go to actually use it? Well, my final flourish was to find a domain name I thought summed up how I felt about the whole experience:</p><blockquote><p><a href="https://rssisawesome.com">rssisawesome.com</a></p></blockquote>So long, and thanks for all the Sass2024-01-29T00:00:00Zhttps://tomhazledine.com/so-long-and-thanks-for-all-the-sass/<p>A strange thing just happened. I was starting a new web project and setting up my build pipeline (and yes, I'm the kind of nerd who enjoys that kind of thing), and I <strong>didn't</strong> import my boilerplate <code>.scss</code> templates. This is probably the first time I've not started a project by importing Sass in, oh, about ten years.</p><nav class="toc"><h2>In this article</h2><ol><li class="stack--small"><a href="#what-is-sassscss-and-why-did-i-need-it-in-the-first-place">What is Sass/SCSS and why did I need it in the first place? <span class="icon icon--link"/></a></li><li class="stack--small"><a href="#why-dont-i-need-to-use-sass-now">Why don't I need to use Sass now? <span class="icon icon--link"/></a></li><li class="stack--small"><a href="#what-did-i-need-to-change-and-refactor">What did I need to change and refactor? <span class="icon icon--link"/></a><ol class="stack--small"><li class="stack--small"><a href="#comments">Comments <span class="icon icon--link"/></a></li><li class="stack--small"><a href="#variables">Variables <span class="icon icon--link"/></a></li><li class="stack--small"><a href="#breakpoints-mixin">Breakpoints mixin <span class="icon icon--link"/></a></li><li class="stack--small"><a href="#imports-and-partials">Imports and partials <span class="icon icon--link"/></a></li><li class="stack--small"><a href="#nesting">Nesting <span class="icon icon--link"/></a></li><li class="stack--small"><a href="#colour-functions">Colour functions <span class="icon icon--link"/></a></li></ol></li><li class="stack--small"><a href="#maybe-dont-just-ship-it-though">Maybe don’t just ship it, though... <span class="icon icon--link"/></a><ol class="stack--small"><li class="stack--small"><a href="#its-probably-still-worth-bundling-your-assets">It’s probably still worth bundling your assets <span class="icon icon--link"/></a></li><li class="stack--small"><a href="#and-while-youre-at-it-why-not-minify-the-code-as-well">And while you’re at it, why not minify the code as well <span class="icon icon--link"/></a></li><li class="stack--small"><a href="#my-esbuild-mvp-config-for-bundling-and-minifiying-css-files">My esbuild “MVP” config for bundling and minifiying CSS files <span class="icon icon--link"/></a></li></ol></li><li class="stack--small"><a href="#what-have-i-learnt">What have I learnt? <span class="icon icon--link"/></a></li></ol></nav><h2 id="what-is-sassscss-and-why-did-i-need-it-in-the-first-place">What is Sass/SCSS and why did I need it in the first place?</h2><p>CSS is how I got into "programming" in the first place (<a href="https://www.codecademy.com/resources/blog/myspace-and-the-coding-legacy/">thank you, MySpace!</a>), and I've always loved doing <a href="https://codepen.io/tomhazledine/full/RwxeVw">fun</a> and <a href="https://codepen.io/tomhazledine/full/GodQxW">weird</a> and <a href="https://codepen.io/tomhazledine/full/VKvNJg">over-engineered</a> things with CSS. But a big part of what made me confident that CSS was a professional tool I could build a career around was being able to enhance my workflow with "pre-processing".</p><p>CSS was always powerful, but it came with quirks and edge-cases and a lot of tiny papercuts. Using a preprocesser took a lot of that pain away and made the developer experience delightful. <strong><a href="https://sass-lang.com/">Sass</a> helped me fall in love with frontend development.</strong></p><p>Simply put, Sass was a tool that let me write the CSS I <em>wanted</em> to write and would then transform it into a "proper" stylesheet that any web browser could understand. Rather than writing <code>styles.css</code>, I'd name my file <code>styles.scss</code> and get instant access to all sorts of fancy extra features.</p><blockquote><p><strong>Side note:</strong> <code>SCSS</code> was the Sass syntax I always preferred, because it more closely resembled vanilla CSS in its formatting - all CSS is valid SCSS, whereas the more uncommon <code>.sass</code> syntax removed braces and semicolons.</p></blockquote><p>Using Sass meant I could use variables for colours and other often-repeated values. I could use logic and perform calculations and write "mixins" that meant I could reuse blocks of functionality without repeating myself too much. And I could nest selectors and break my code into smaller files that were easier to read and maintain.</p><p>So much of that sounds trivial nowadays, but back in 2012 when I was first learning about these concepts it felt revolutionary. I'd already been tinkering with CSS for years at that point, but learning Sass was the first time I ever felt more like a "programmer" than a "designer".</p><h2 id="why-dont-i-need-to-use-sass-now">Why don't I need to use Sass now?</h2><p>Over the years my use of Sass evolved to the point where I'd built up a solid "starter kit" of Sass conventions and tricks that I found useful in almost every project. Rather than revisiting a load of decisions and choices with every new project, I could get to "meaningful work" much faster. I'd update the starter kit periodically as I learnt new things or discovered new ways to tackle old problems. But, as is often the case with foundational things that become part of your muscle memory, I tended to be a bit slow in <em>removing</em> deprecated or redundant parts. It "just worked" so why invest time in changing it too much?</p><p>In the early 2010s it felt like vanilla CSS moved at a glacially slow pace. After the influx of new features in CSS3, things took a long time to trickle out to all browsers and supporting "legacy" browsers was a big deal. But thankfully things are different now. The switch to "evergreen" browsers (ones that automatically update without their users having to do anything) was a massive step forward, as were the introduction of features like custom properties (a.k.a. variables) and grid systems and imports. <strong>CSS is great now, and we can pretty much use all the fun stuff everywhere!</strong></p><p>I could probably have dropped my Sass starter-kit a few years ago, and a lot of my work projects have been Sass-free for even longer. But this week was the first time I started a new side project and actively avoided adding Sass. Rather than blindly importing the starter-kit just as I always had done before, I instead refactored the whole thing to use vanilla CSS.</p><h2 id="what-did-i-need-to-change-and-refactor">What did I need to change and refactor?</h2><p>The Sass-to-CSS refactor was delightfully straightforward, but it wasn't just a case of renaming the file extensions.</p><h3 id="comments">Comments</h3><p>The first big find-and-replace job was to tackle code comments. In Sass you can use a double forward slash to mark any following text on the line as a comment just like in JavaScript: <code>// text</code>. But in vanilla CSS you can only use <code>/* text */</code>. Crucially, the vanilla version requires the closing <code>*/</code> at the end of the comment. Honestly this might be the biggest thing I miss from Sass, and I was surprised by how much the closing-tag requirement annoyed me. But on the plus side it's encouraging me to be more diligent about removing lines I'd commented-out "temporarily" (and which had inevitably remained in my code for years).</p><h3 id="variables">Variables</h3><p>In Sass a variable is defined with the <code>$name: value;</code> syntax. 10 years ago this was an absolute game changer, but vanilla CSS has had "custom properties" for years now. Because custom properties are part of the vanilla spec, they can be altered and scoped at run-time, which actually makes them <em>more</em> powerful than Sass variables.</p><p>There is a case to be made that complex calculations using variables are better done in Sass because all the work happens at build-time (whereas custom properties are worked out by browsers on the fly when the code is run), but in the vast majority of use cases (and, in fact, in <em>all</em> of mine) CSS custom properties are a better choice.</p><p>Aside from having to add a "scope" to the variable declarations (Sass vars can be defined anywhere and everywhere within a SCSS file, whereas CSS custom properties need to be defined within a selector) this was a simple refactor. In fact, this is a change I made a long time ago (back when <a href="https://tomhazledine.com/dark-mode">"dark mode"</a> became a common requirement). Since then, all my colour and type vars have been custom properties, and this recent refactor of my starter kit caught a few extra spacing variables that I had forgotten to convert.</p><pre><code class="hljs language-scss"><span class="hljs-variable">$colour-red</span>: <span class="hljs-number">#ff4136</span>;
<span class="hljs-selector-class">.foo</span> {
<span class="hljs-attribute">color</span>: <span class="hljs-variable">$colour-red</span>;
}
</code></pre><pre><code class="hljs language-css"><span class="hljs-selector-pseudo">:root</span> {
<span class="hljs-attr">--colour-red</span>: <span class="hljs-number">#ff4136</span>;
}
<span class="hljs-selector-class">.foo</span> {
<span class="hljs-attribute">color</span>: <span class="hljs-built_in">var</span>(--colour-red);
}
</code></pre><h3 id="breakpoints-mixin">Breakpoints mixin</h3><p>Early in my Sass days I adopted a breakpoint "mixin" strategy for standardising my responsive breakpoints. A mixin is a reusable block of code that can be included (or "mixed in") throughout a stylesheet, so rather than YOLO'ing a new media query every time I needed one I could use my <code>bp($size)</code> mixin to target specific breakpoints.</p><pre><code class="hljs language-scss"><span class="hljs-comment">// My lengthly (and now completely redundant) breakpoint mixin:</span>
<span class="hljs-keyword">@mixin</span> bp(<span class="hljs-variable">$point</span>) {
<span class="hljs-comment">// "min-width" breakpoints (screens getting bigger)</span>
<span class="hljs-keyword">@if</span> <span class="hljs-variable">$point</span> == u1 {
<span class="hljs-keyword">@media</span> (<span class="hljs-attribute">min-width</span>: <span class="hljs-number">960px</span>) {
<span class="hljs-keyword">@content</span>;
}
} <span class="hljs-keyword">@else</span> if <span class="hljs-variable">$point</span> == u2 {
<span class="hljs-keyword">@media</span> (<span class="hljs-attribute">min-width</span>: <span class="hljs-number">1200px</span>) {
<span class="hljs-keyword">@content</span>;
}
} <span class="hljs-keyword">@else</span> if <span class="hljs-variable">$point</span> == u3 {
<span class="hljs-comment">// etc...</span>
}
<span class="hljs-comment">// "max-width" breakpoints (screens getting smaller)</span>
<span class="hljs-keyword">@else</span> if <span class="hljs-variable">$point</span> == d1 {
<span class="hljs-keyword">@media</span> (<span class="hljs-attribute">max-width</span>: <span class="hljs-number">1000px</span>) {
<span class="hljs-keyword">@content</span>;
}
} <span class="hljs-keyword">@else</span> if <span class="hljs-variable">$point</span> == d2 {
<span class="hljs-comment">// etc...</span>
}
}
<span class="hljs-comment">// The mixin in use:</span>
<span class="hljs-selector-class">.related-llm-icon</span> {
<span class="hljs-attribute">width</span>: <span class="hljs-number">80%</span>;
<span class="hljs-keyword">@include</span> bp(u2) {
<span class="hljs-attribute">width</span>: <span class="hljs-number">50%</span>;
}
}
</code></pre><p>At first glance, this seems like an ideal use for custom properties. At the expence of a few extra characters (i.e. writing a full <code>@media (min-width...</code> statement) I could have pretty much the same functionallity as with the original mixin. <em>(Beware - this is a trap!)</em></p><pre><code class="hljs language-css"><span class="hljs-comment">/* Breakpoint variables: */</span>
<span class="hljs-comment">/* <span class="hljs-doctag">Note:</span> THIS DOES NOT WORK! */</span>
<span class="hljs-selector-pseudo">:root</span> {
<span class="hljs-attr">--breakpoint-1</span>: <span class="hljs-number">960px</span>;
<span class="hljs-attr">--breakpoint-2</span>: <span class="hljs-number">1200px</span>
}
<span class="hljs-comment">/* Using a breakpoint: */</span>
<span class="hljs-comment">/* <span class="hljs-doctag">Note:</span> THIS DOES NOT WORK! */</span>
<span class="hljs-selector-class">.responsive-thing</span> {
<span class="hljs-attribute">width</span>: <span class="hljs-number">80%</span>;
<span class="hljs-keyword">@media</span> (<span class="hljs-attribute">min-width</span>: var(--breakpoint-<span class="hljs-number">2</span>)) {
<span class="hljs-attribute">width</span>: <span class="hljs-number">50%</span>;
}
}
</code></pre><blockquote><p>⚠️ I was totally wrong about this! You <strong>cannot</strong> use custom properties in media queries. I spent way longer than I'd care to admit debugging why my media queries weren't being applied.</p></blockquote><p>At the time of writing, I haven't cracked this issue. I'd love to be able to set my breakpoint values in a single place without having to resort to any pre- or post-processing, but for now I'm back to copy/pasting good ol' Magic Numbers.</p><pre><code class="hljs language-css"><span class="hljs-selector-class">.responsive-thing</span> {
<span class="hljs-attribute">width</span>: <span class="hljs-number">80%</span>;
<span class="hljs-keyword">@media</span> (<span class="hljs-attribute">min-width</span>: <span class="hljs-number">1200px</span>) {
<span class="hljs-attribute">width</span>: <span class="hljs-number">50%</span>;
}
}
</code></pre><h3 id="imports-and-partials">Imports and partials</h3><p>I was expecting some difficulty when restructuring my Sass partials into vanilla CSS files, but it was actually the easiest part of the whole process. Vanilla CSS's <code>@import</code> syntax is identical to that of Sass, so all I needed to do was add the <code>.css</code> file extension to each import path.</p><p>Importing lots of partials into a single production file was a "big win" when I switched from vanilla to Sass, but looking back at the <a href="https://caniuse.com/?search=%40import">caniuse compatibility table for <code>@import</code></a> makes me wonder why I thought I needed Sass for this. I don't regret my choice, as I think build-time bundling is <em>still</em> a good idea (see my <a href="#its-probably-still-worth-bundling-your-assets">later section about bundling</a>) and back in 2012 my Sass pipeline was the only way I knew how to do that kind of thing. Thankfully now (as we'll see in a few paragraphs) there are much simpler ways to bundle and minify your CSS.</p><h3 id="nesting">Nesting</h3><p>The ability to nest selectors in Sass was a much bigger deal for me when I started than it is now. Back then, the idea of nesting selectors aligned with my mental model for structuring my style rules. Over time, that mental model has shifted; first to limiting nesting to 2 or 3 levels deep, and then with my adoption of BEM it pretty much disappeared completely.</p><p>If you'd asked me last week, I'd have said I hardly ever nest selectors at all anymore. That said, it was interesting to see during this latest refactor exactly how much nesting I still had in my starter kit. Not loads, but enough that I had to spend a bit of time copy/pasting root classes onto the start of selectors. None of this substantially changed the structure of my stylesheets, but maybe a little bit of nesting <em>does</em> still fit with how I think about CSS. With that in mind, I might be open to a future where I set up the tooling to reintroduce nesting into my workflow.</p><h3 id="colour-functions">Colour functions</h3><p>Sass has some build-in functions for manipulating colours, and I used them a lot. The most common ones I used were <code>darken()</code> and lighten, which would take a colour (hex-code, string name, whatever you were using would work) and make it darker or lighter by the specified amount. <code>darken($colour, 20%)</code> was my go-to way to set the colours for hover states and dropshadows and meant I didn't have to manually calculate the new colour values.</p><p>I <strong>do</strong> miss the interoperability of colour formats in Sass (being able to mix and match <code>#ff4136</code> and <code>red</code> and <code>rgb(255, 65, 54)</code> within the same context was great), but I've gradually retrained myself to simply put in a bit more work when defining my colour variables. In places where I would have used <code>darken()</code> or <code>lighten()</code> I now use <code>hsl()</code>, where the "lightness" value of the hue/saturation/lightness format behaves just the same as using a percentage value in <code>darken</code> or <code>lighten</code>. And with tools like Copilot on hand to do the conversion from hex to hsl for me, it's not even that much extra work.</p><h2 id="maybe-dont-just-ship-it-though">Maybe don’t just ship it, though...</h2><p>Despite the fact that you <em>can</em> responsibly ship un-processed CSS files with your project, for any large (normal?) sized project you’d be leaving money on the table if you didn’t put <em>some</em> effort into optimisation.</p><h3 id="its-probably-still-worth-bundling-your-assets">It’s probably still worth bundling your assets</h3><p>If you’re not bundling your code, every <code>@import</code> statement means the browser has to make another request. Bundling (a.k.a. smushing all your code together into one <a href="https://knowyourmeme.com/memes/big-chungus">Big Chungus</a> of a file) means the browser needs to make fewer requests to your server in order to render your page, so in most cases “fewer requests” means “faster page load”.</p><p>Obviously part of the joy of switching to a mostly-vanilla workflow is that you don’t have to faff about with build tools anymore, so adding a bundling step <strong>is</strong> adding complexity to your project. But the options are so much simpler, quicker, and easier to setup than they were ten years ago.</p><blockquote><p>I recommend esbuild for your bundling/post-processing tasks, but I <strong>strongly</strong> believe that if you’re already using another tool (for your JavaScript, or images, or whatever) then you should add your stylesheet processing into <em>that</em> tool rather than spend ages refactoring.</p><p>Vanilla CSS optimisation and processing is table-stakes for tools like <a href="https://webpack.js.org/">webpack</a> and <a href="https://parceljs.org/">Parcel</a> and <a href="https://vitejs.dev/">Vite</a> and <a href="https://www.snowpack.dev/">Snowpack</a> and all the rest... Whichever you choose, you’ll be fine.</p></blockquote><h3 id="and-while-youre-at-it-why-not-minify-the-code-as-well">And while you’re at it, why not minify the code as well</h3><p>If you’ve gone to the effort of bundling, it’s generally a trivial extra step to add minification to your build as well. Minification reduces the file size of your CSS by eliminating unnecessary characters (whitespace, line breaks, comments, etc.) without altering the functionality.</p><p>Some unminified CSS:</p><pre><code class="hljs language-css"><span class="hljs-comment">/* Main container style */</span>
<span class="hljs-selector-class">.container</span> {
<span class="hljs-attribute">width</span>: <span class="hljs-number">80%</span>;
<span class="hljs-attribute">margin</span>: <span class="hljs-number">0</span> auto;
<span class="hljs-attribute">padding</span>: <span class="hljs-number">20px</span>;
}
<span class="hljs-comment">/* Header style */</span>
<span class="hljs-selector-class">.header</span> {
<span class="hljs-attribute">background-color</span>: <span class="hljs-number">#f3f3f3</span>;
<span class="hljs-attribute">padding</span>: <span class="hljs-number">10px</span>;
<span class="hljs-attribute">font-size</span>: <span class="hljs-number">24px</span>;
<span class="hljs-attribute">border</span>: <span class="hljs-number">1px</span> solid <span class="hljs-number">#d1d1d1</span>;
}
</code></pre><p>The same code, after being minified *(note that as far as the computers are concerned, <a href="https://knowyourmeme.com/memes/theyre-the-same-picture">they’re the same picture</a>):</p><pre><code class="hljs language-css"><span class="hljs-selector-class">.container</span>{<span class="hljs-attribute">width</span>:<span class="hljs-number">80%</span>;<span class="hljs-attribute">margin</span>:<span class="hljs-number">0</span> auto;<span class="hljs-attribute">padding</span>:<span class="hljs-number">20px</span>}<span class="hljs-selector-class">.header</span>{<span class="hljs-attribute">background-color</span>:<span class="hljs-number">#f3f3f3</span>;<span class="hljs-attribute">padding</span>:<span class="hljs-number">10px</span>;<span class="hljs-attribute">font-size</span>:<span class="hljs-number">24px</span>;<span class="hljs-attribute">border</span>:<span class="hljs-number">1px</span> solid <span class="hljs-number">#d1d1d1</span>}
</code></pre><h3 id="my-esbuild-mvp-config-for-bundling-and-minifiying-css-files">My esbuild “MVP” config for bundling and minifiying CSS files</h3><p>My setup uses <a href="https://esbuild.github.io/">esbuild</a>, and compared to my previous pipelines is hilariously simple (and replaces ~140 lines of webpack config or ~200 of <code>gulpfile.js</code> if you’re feeling <em>really</em> nostalgic).</p><pre><code class="hljs language-js"><span class="hljs-comment">// build.js</span>
<span class="hljs-keyword">import</span> { build } <span class="hljs-keyword">from</span> <span class="hljs-string">"esbuild"</span>;
<span class="hljs-title function_">build</span>({
<span class="hljs-attr">entryPoints</span>: [<span class="hljs-string">'app.css'</span>],
<span class="hljs-attr">outfile</span>: <span class="hljs-string">'out.css'</span>,
<span class="hljs-attr">bundle</span>: <span class="hljs-literal">true</span>,
<span class="hljs-attr">minify</span>: <span class="hljs-literal">true</span>,
});
</code></pre><p>Okay, okay... my real setup is a bit more involved than that. It has flags to handle “development” and “production” builds differently, and will watch files for changes and create source maps and smartly rename multiple source files and a whole load of other useful things.</p><p>But the point is: that small snippet is all you <strong>need</strong>. </p><p>Install esbuild and add that <code>build.js</code> file and you’re pretty much good-to-go. Rename the <code>entryPoints</code> and <code>outfile</code> paths to match your folder structure and you can create production-ready CSS by running <code>node build.js</code>.</p><p>It’s not as straightforward as shipping vanilla, un-processed CSS files, but on balance probably still worth the effort (and it’s a hell of a lot less effort than it used to be!).</p><h2 id="what-have-i-learnt">What have I learnt?</h2><p>My biggest takeaway from this exercise is that <strong>I should have done this much sooner!</strong> Sass is one of those tools that I'd been using for so long that it had become second nature, and I'd stopped even thinking about it. Let alone thinking about if I still needed it. It's so easy to sleep on these "base layer" parts of my tooling, even though it's something I'd interact with almost every day. Now I need to evaluate the rest of my setup and see if there are any other parts in need of a good spring cleaning.</p><p>But either way, Sass was an important part of my developer journey and I'll always have fond memories of us ing it. So long, Sass, and <a href="https://youtu.be/N_dUmDBfp6k?si=pbSDltIAVNraoP1i&t=30">thanks for all the fish</a>.</p>Mapping LLM embeddings in three dimensions2023-12-30T00:00:00Zhttps://tomhazledine.com/mapping-llm-embeddings-in-3d/<p>Embeddings are long lists of seemingly inscrutable numbers created by an LLM, but if you're prepared to lose a little precision you can force them into 3D space where they're a little easier to comprehend and explain.</p><figure class="post-content__image-wrapper"><div id="embedding-example"/><figcaption class="post-content__caption"><p>fig #1: <strong>Headlines from a galaxy far, far away.</strong></p><p>Plotting the semantic "meaning" of strings of text that have been converted to embeddings by an LLM. Hover over points to see the original text value and use the "rotation" slider to create a parallax effect.</p></figcaption></figure><h2 id="what-am-i-trying-to-show-here">What am I trying to show here?</h2><p>I talked a lot about "embeddings" in a recent article about <a href="https://tomhazledine.com/llm-related-posts">finding related posts within my blog archive</a>. In that article I showed how embeddings could be used to calculate similarities between different chunks of text, and while writing it I had to work out how best to describe what embeddings actually are.</p><p>The most obvious description I found for embeddings was the one most closely tied to how I was using them; I was using them to calculate how "close" two piece of text were to each other, so that naturally led to relying on <strong>physical space</strong> as a useful metaphor. Sure, the embeddings I use are coordinates in fifteen-hundred-dimensional space, but if you squint your eyes <em>just so</em> you can shrug and say <em>"coordinates are coordinates"</em>. As I said in that article:</p><blockquote><p><strong>"by using embeddings you're creating a 'map' of the meaning of your content"</strong></p><p><em>Me, <a href="https://tomhazledine.com/llm-related-posts">a few weeks ago</a></em></p></blockquote><p>Over the past few months I've found this to be a really helpful way to think about embeddings, and I've been describing this theoretical "map" whenever I've mansplained embeddings to people (apologies if you've met me lately and been bored stiff by my constant LLM talk).</p><h2 id="how-can-we-actually-visualise-this-map">How can we <em>actually</em> visualise this map?</h2><p>It's all well and good to describe a theoretical multi-dimensional "map of content", but quite another to come up with a way to visualise it without access to a <a href="https://en.wikipedia.org/wiki/Hypercube">tesseract or any other n-dimensional hypercube</a>. As I mentioned in the previous article, I've been using OpenAI's <code>ada-002</code> model to create my embeddings, and the embeddings it creates are <a href="https://byjus.com/maths/what-is-vector/">"vectors"</a> made up of 1,536 numbers. To plot these vectors on a graph you'd need to (somehow) visualise 1,536-dimensional space. That's an impossibility, so the only <em>practical</em> option is to reduce the number of dimensions we're dealing with. Thankfully there is a mathematical tool we can put to use to achieve this, provided we're happy to lose some data in the process.</p><p><strong><a href="https://en.wikipedia.org/wiki/Principal_component_analysis">Principle component analysis</a></strong> is a "dimensionality reduction" technique that allows us to take a large-dimensional dataset and convert it into a smaller set of dimensions. This is a new technique to me (and I copy/pasted most of the code I used) but to the best of my understanding a "principal component" can be seen as a "direction" in multidimensional space. In a 3D space, the directions are up/down, left/right, forward/backward (the classic <code>x</code>, <code>y</code>, <code>z</code> coordinates). In 4D, 5D, n-D space, the same idea applies. Principle Component Analysis (PCA) works out which dimensions have the most variance and ignores the ones with less variance.</p><p>It's an inherently lossy processes as we're literally ignoring parts of the data, but it's a good way to get the general "vibe" for a dataset. I'm happy listening to MP3s and looking at JPEGs online as the compression benefits outweigh the loss in fidelity. And the embeddings I've been working with represent such a small dataset that I'm happy to lose some detail in exchange for the added comprehensibility.</p><p>The practical implementation of PCA into my project was actually fairly trivial thanks to the <a href="https://mljs.github.io/pca"><code>ml-pca</code> Node.js package</a>, so now I can take my list of embeddings (with each embedding being an array of 1,536 numbers) and transform them into arrays of just three numbers.</p><pre><code class="hljs language-js"><span class="hljs-keyword">import</span> { <span class="hljs-variable constant_">PCA</span> } <span class="hljs-keyword">from</span> <span class="hljs-string">"ml-pca"</span>;
<span class="hljs-keyword">import</span> { embeddings } <span class="hljs-keyword">from</span> <span class="hljs-string">"./my-local-data.js"</span>
<span class="hljs-keyword">const</span> pca = <span class="hljs-keyword">new</span> <span class="hljs-title function_">PCA</span>(embeddings);
<span class="hljs-comment">// Get the first three principal components</span>
<span class="hljs-keyword">const</span> { data } = pca.<span class="hljs-title function_">predict</span>(embeddings, { <span class="hljs-attr">nComponents</span>: <span class="hljs-number">3</span> });
</code></pre><p>I did originally include an example of a <strong>full</strong> embedding here, but it was just too much scrolling even if it was funny to see the full before/after comparison. Suffice it to say, this processes turns a big array into a small array.</p><h2 id="what-does-this-tell-me-about-my-data">What does this tell me about my data?</h2><p>Given that I'd already created an embedding for each of my blog posts, the next natural step was to run a PCA on them and plot them in a 3D scatter graph. The labels on the graph show just the post titles, but the embeddings were calculated from the entire content of each post. The primary category each post belongs to is manually set by me when I write each post, and the colours of the point on the graph each correspond to a specific category.</p><figure class="post-content__image-wrapper"><div id="embedding-vis"/><figcaption class="post-content__caption"><p>fig #1: <strong>A "map" of my blog posts</strong></p><p>Each post's position in space is determined by the embedding of the content (a.k.a. they're arranged by semantic meaning).</p></figcaption></figure><p>This graph is pretty, but currently just serves as a novelty. It was a lot of fun to make, as I've done a lot of 2D SVG graphing but never anything that tried to show three dimensions like this (and I'm rather pleased with how it turned out). I find it fascinating to see "objective" relationships between content I've written over the last ten years, especially the little clusters of topics...</p><p>There are a few massive outliers where I spent a month writing about sports prediction. There's a pleasing "podcasting zone" over to the right and nice little typography corner down the bottom, too. While not being intrinsically useful yet, it's already got me rethinking how I categorise my archive, and also got me thinking about clusters I'd like to flesh out more (and gaps that might need filling!).</p><p>Importantly, what it <em>does</em> do is show visually how my <a href="https://tomhazledine.com/llm-related-posts">cosine similarity calculations</a> were working out which posts were similar. A <strong>LOT</strong> of the dimensions have been removed, but the basic premise is still clearly shown. **This graph is a much more concrete illustration of "embedding space" than my previous hand-wavey, back-of-a-napkin attempts ever were. It also has the benefit of being based on real data (even if it is greatly optimised).</p><blockquote><p>When I'm describing embeddings as a "map of content" I can now show them this graph to make my point more clearly than my bumbling words ever could.</p></blockquote><h2 id="this-is-not-a-true-map-of-embedding-space">This is not a true "map" of embedding space</h2><p>In some ways this is not the post I set out to write.</p><p>When I was first experimenting with PCA, I assumed that the outputs represented absolute values. I though that an embedding for a string of text would always result in the same 3D coordinates and PCA would "simplify" all embedding data in the same way. I naively thought that PCA would allow me to map <em>any</em> embedding into the same 3D space. I was wrong about this.</p><p>Because the principal components are determined by the <em>variance</em> in the data, different data sets will have more or less variance in different dimensions. The PCA "space" that gets created is unique to each set of embeddings that the PCA has been run with. The "map" of my blog posts will be drawn in a very different space to the map of <em>your</em> blog posts, so we couldn't combine the two sets of 3D coords. But if we combined our embeddings and ran a PCA for all of them at the same time then we <em>would</em> have a map of our combined posts but the positions wouldn't match the original independent maps.</p><p>My original plan was to build the graph framework and have a form input where anyone could type in some text and see it added into the embedding space. I thought I could make a real map of all embeddings. <strong>PCA just doesn't work like that</strong>. I <em>could</em> (and might yet?!) create a page where you can input a whole set of text strings and have it calculate the embeddings and PCA'd 3D map in real-time, but what with building the SVG cube graph from scratch I've already burned more time on this post that I intended. That'll have to wait for a future project.</p><h2 id="what-next">What next?</h2><p>Aside from building the interactive version of the graph, what are the next steps for this? Crucially, <strong>how can I turn this curiosity into something genuinely useful</strong>?</p><h3 id="fancy-data-vis">Fancy data vis</h3><p>I love a good piece of data visualisation, and embeddings open up a host of new areas for me to experiment with. And if I use embeddings and PCA to generate "spatial" graphs and charts, it's a small step to pair that with other factors to tell interesting stories with data.</p><h3 id="search-index-size-optimisation">Search index size optimisation</h3><p>Following on from my "related posts" experiments, I've been testing out embeddings as the basis for a genuine site-search engine. It "kind of" works currently, but the biggest issue is that embeddings are so damn large! My current set of "whole file" embeddings comes in at about 1.6 MB, which is more than three times the size of my real search index (which, at 419 KB contains all the text content of all my pages). Yep, the JSON file with a single embedding for each post is larger than the actual content of all those posts. And to get search results that compete with, say, Fuse.js' matching engine I'd need even more embeddings; ideally one per paragraph of text. Totally un-useable for the statically-generated client-side-only approach I prefer.</p><p>Applying principle component analysis to those embeddings might (maybe) change that. I doubt three dimensions will produce good search results, but perhaps even halving the number of dimensions might still produce useable results while being a more manageable size. I'll definitely be doing more experimentation around this in the coming weeks.</p><p>Wherever I go with this next, I'm having a lot of fun and I'm definitely not finished yet.</p>Publishing on npm is weird2023-11-30T00:00:00Zhttps://tomhazledine.com/npm-publishing/<p>This is a list of a few things I’ve learned while publishing my JavaScript shenanigans on npm. Or, more accurately, it's a list of things I’ve banged my head against while <em>trying</em> to publish on npm. Some of it is logical, some of it is inscrutable. All of it was necessary. I’m documenting it here so that next time I go to publish something I can avoid going down a hundred-open-tabs search-engine rabbit hole (again!).</p>
<h2 id="the-scenario" tabindex="-1">The scenario</h2>
<p>I really <a href="https://tomhazledine.com/wordle-node-script/">enjoy</a> <a href="https://tomhazledine.com/oblique-strategies-via-npx/">writing</a> <a href="https://tomhazledine.com/llm-related-posts/">scripts</a> in Node. I long ago accepted the fact that JavaScript is the language I’m most fluent in, and now I “lean in” to being able to write JS <em>everywhere</em>. Other people might use Bash or Python to solve their little problems but when I want to automate something or inspect some data, I turn to Node.js.</p>
<p>And when I want to share my work, the <a href="https://www.npmjs.com/">npm</a> registry is the first place I think of. It’s great to be able to run <code>yarn add my-thing</code> (or <code>npm install my thing</code> if you prefer) and instantly be able to reuse my little scripts.</p>
<p>So what has consistently tripped me up? And what have I learned after publishing a few things?</p>
<h2 id="put-it-in-the-bin" tabindex="-1">Put it in the bin</h2>
<p>If a script is to be run by an end user (i.e. they write <code>npm run your-cool-script</code> or <code>yarn your-cool-script</code>), you need to add that script to the <code>bin</code> section of your <code>package.json</code></p>
<pre><code class="language-json">{
"bin": {
"your-cool-script": "./path/to/the/script.js"
}
}
</code></pre>
<h2 id="type-%3D-module" tabindex="-1">Type = module</h2>
<p>If you’re writing using ESM syntax (<code>import foo, { bar } from “baz”</code>, etc...), you need to declare your package as a “module” in your <code>package.json</code></p>
<pre><code class="language-json">{
"type": "module"
}
</code></pre>
<h2 id="shebang" tabindex="-1">Shebang</h2>
<p>If you want your script to run correctly on Unix-like systems (which MacOS and Linux both are) then the file should start with a “shebang”: <code>#!/usr/bin/env node</code>.</p>
<p>This is a comment that specifies which interpreter the system should use for the script, and uses the user’s <code>env</code> command to find the <code>node</code> interpreter in the system’s <code>PATH</code>. Note that <code>bin</code> is <a href="#put-it-in-the-bin">again</a> being used as a place to store executable scripts, this time at a system level.</p>
<p>Shebang get’s its name from a concatenation of “sharp” (i.e. <code>#</code>) and “bang” (i.e. <code>!</code>) as interpreter comments all start with <code>#!</code>.</p>
<p>If you’re using a tool like <code>esbuild</code> to compile your script from multiple source files, then you’ll need to make use of the “banner” configuration option. Setting a “banner” is a way to inject arbitrary text at the start of your final built file.</p>
<pre><code class="language-js">const esbuildConfig = {
// ...
banner: {
js: "#!/usr/bin/env node"
}
};
</code></pre>
<h2 id="esbuild%E2%80%99s-format-and-platform" tabindex="-1">esbuild’s format and platform</h2>
<p>Speaking of esbuild, there are a couple of other configuration options that you should set: <code>format</code> and <code>platform</code>.</p>
<p>You need to set <code>format</code> to be “esm” because even now at the end of 2023 ESM is <em>still</em> not the default. CommonJS just will not die, and it will never cease to confound me as to why.</p>
<p>Setting <code>platform</code> to “node” makes more sense, however, as the esbuild is primarily a tool for building things for the web. By setting this config value we’re telling it not to worry too much about built-in Node packages like <code>path</code> and <code>fs</code> (which it would freak out about if we tried to compile those for the web).</p>
<pre><code class="language-js">const esbuildConfig = {
// ...
format: "esm",
platform: "node",
banner: {
js: "#!/usr/bin/env node"
}
};
</code></pre>
<h2 id="let-the-npm-cli-do-the-work" tabindex="-1">Let the npm CLI do the work</h2>
<p>Versioning is an important part of reliably releasing your code, especially if you’re like me and constantly George Lucas-ing your creations. <a href="https://semver.org/">SemVer</a> (a.k.a. <strong>sem</strong>antic <strong>ver</strong>sioning) is our friend here, so stick to the <code>{major}.{minor}.{patch}</code> format.</p>
<p>I very quickly learned to stop manually tweaking the <code>version</code> value in my <code>package.json</code>. It’s much more straightforward to run <code>npm version {major|minor|patch}</code>. This takes care of bumping the package version and cutting a new “tag”. If these get out of sync, then you’ll have problems when pushing the code to npm, so the CLI is a much more reliable way to handle this process. Just remember to <code>git push --tags</code> to ensure the new tag gets pushed to your repo.</p>
<p><em><strong>Note:</strong> Watch out if you’re in a sub-folder of a monorepo, <code>npm version</code> won’t do the tagging and commit for you!</em></p>
<h2 id="always-start-at-zero" tabindex="-1">Always start at zero</h2>
<p>As an aside to versioning, I find it helpful to always start with a “major” version of <code>0</code> (so commit #1 is usually version <code>0.1.0</code>). When in the early stages of a new project I’m frequently adding “breaking changes” as I find my feet, so if I struck strictly to SemVer and began at <code>v1.0.0</code> I’d be on version three hundred before my code ever saw the light of day.</p>
<p>The pre-version-one freedom to play fast-and-loose with backwards compatibility is essential, in my opinion.</p>
<h2 id="go-forth-and-publish" tabindex="-1">Go forth and publish</h2>
<p>This is not a comprehensive list of <em>All The Things You Need To Know To Publish On npm™️</em>, but everything include here is something I’ve had to learn through trial and error. I’ll hit more obstacles, I’m sure, and when I do I might remember to add them to this page (maybe). But at least I’ve got these ones out of my working memory and into long term storage. And is that not the main purpose of technical blogging after all?</p>TomBot2000: automatically finding related posts using LLMs2023-10-27T00:00:00Zhttps://tomhazledine.com/llm-related-posts/<p>I've been having a lot of fun lately with <strong>embeddings</strong>. They are a somewhat-overlooked sideshow to the current GPT/LLM/AI circus, but in my view they are even more interesting than the headline-grabbing chat apps. And unlike the the chat interfaces, embeddings are a way to make LLMs <em>actually useful</em> without any of the worries about "hallucination" or <em>just-plain-wrong</em> content.</p><p>A large part of what I've been using embeddings for is "natural language search" which (at the simplest level) involves defining how similar two bits of text are. Give the robots two different pieces of content and they then decide how similar the "meanings" of the two pieces are on a scale of <code>0.0</code> to <code>1.0</code>. This is great for search applications where it neatly sidesteps the need for complex algorithms and complicated matching engines (rendering a lot of my <a href="https://tomhazledine.com/client-side-search-static-site/">previous search work</a> charmingly obsolete!), but it's also a neat solution to a problem that I've been halfheartedly grappling with for years: <strong>finding "related posts" for blog content</strong>.</p><nav class="toc"><h2>In this article</h2><ol><li class="stack--small"><a href="#why-do-i-care-about-related-posts">Why do I care about related posts? <span class="icon icon--link"/></a></li><li class="stack--small"><a href="#how-have-i-tried-and-failed-to-generate-related-posts-in-the-past">How have I tried and failed to generate related posts in the past? <span class="icon icon--link"/></a><ol class="stack--small"><li class="stack--small"><a href="#failed-attempt-1-the-manual-approach">Failed attempt #1: the manual approach <span class="icon icon--link"/></a></li><li class="stack--small"><a href="#failed-attempt-2-leaning-on-cms-features">Failed attempt #2: leaning on CMS features <span class="icon icon--link"/></a></li></ol></li><li class="stack--small"><a href="#so-what-are-embeddings">So what are embeddings? <span class="icon icon--link"/></a></li><li class="stack--small"><a href="#using-embeddings-for-natural-language-search">Using embeddings for "natural language search" <span class="icon icon--link"/></a></li><li class="stack--small"><a href="#can-embeddings-be-used-to-calculate-relations-between-our-content">Can embeddings be used to calculate relations between our content? <span class="icon icon--link"/></a></li><li class="stack--small"><a href="#calculating-the-related-posts">Calculating the related posts <span class="icon icon--link"/></a></li><li class="stack--small"><a href="#gotchas">Gotchas <span class="icon icon--link"/></a></li><li class="stack--small"><a href="#script-optimisation">Script optimisation <span class="icon icon--link"/></a></li><li class="stack--small"><a href="#ship-it">Ship it! <span class="icon icon--link"/></a></li></ol></nav><h2 id="why-do-i-care-about-related-posts">Why do I care about related posts?</h2><p>When I talk about "related posts", what I'm referring to is the section at the end of an article that points the reader to other, similar content. The promise being made to any reader that reaches the end of an article or post is: <strong><em>"If you liked reading that, then you'll probably be interested in this too"</em></strong>.</p><p>Partly I want to make my sites more useful to people who visit them, but <em>mostly</em> I just want to keep readers on my site. I've been working on blog-style content sites for well over a decade-and-a-half at this point, and I'm keenly aware of how tough a challenge "audience retention" can be.</p><p>The default option of "next post" and "previous post" links (I default option I've often used myself) doesn't really provide much value. Only the most ardent completionists are going to work their way through a site from the very start to the very end. If you can surface <em>related</em> content instead, that's inevitably going to be more useful.</p><h2 id="how-have-i-tried-and-failed-to-generate-related-posts-in-the-past">How have I tried and failed to generate related posts in the past?</h2><h3 id="failed-attempt-1-the-manual-approach">Failed attempt #1: the manual approach</h3><p>The "easiest" way to add related content to the bottom of a post is to do it manually. By which I mean picking a couple of posts that you have personally decided are related to the post you've just written. Then you type the links out at the end of the post's content yourself. You, the post's author, do the choosing. You find the links. You write the description. In many ways this is probably the <em>best</em> way to do it, but there are a few issues:</p><ol><li><strong>You can only choose from content that already exists.</strong> This is because the related content you select is limited to the articles or posts you've already published, leaving no room for future publications to be included. In theory, you could revisit every post every time you add a new one and update the related links, but this quickly becomes impractical and time-consuming.</li><li><strong>The links you choose might break.</strong> Once you have hand-written a related post link, that link is locked in place. If the permalink for the related post changes (for example, if you update the title of the post or change the structure of your site) then you need to remember to come back and update the link (or at the very least setup a 303 redirect). Again, this is lots of work, and an almost certain recipe for "dead links" once your site reaches any kind of scale.</li><li><strong>It's a lot of work.</strong> Both issues 1. and 2. involve a lot of manual effort, but there's also the cognitive overhead of keeping all your posts' content in your head. Plus it takes a lot of time and brainpower to come up with the "relations" in the first place.</li></ol><p>In short, choosing related posts is a process that cries out for automation.</p><h3 id="failed-attempt-2-leaning-on-cms-features">Failed attempt #2: leaning on CMS features</h3><p>One massive time saving that can be made is to define "related posts" in whatever CMS you're using. Back (way back!) when I used WordPress, it was relatively trivial to add a custom field to the edit screen that allowed me to select any existing post from a dropdown. With that mechanic in place it's possible to <em>quickly</em> select a "related" post (or several) whenever a new post is written.</p><p>This is still a <em>slightly</em> manual process because you still have to choose the relations, but it does avoid some of the pitfalls of an entirely manual process:</p><ul><li>By selecting a post from within the CMS rather than by pasting a URL string, the relation is dynamically linked to the post object (meaning if the title or permalink change, the "related" link inherits those changes).</li><li>Because this approach uses the post object there are also more possibilities at the presentational level, as templates can access <em>any</em> part of the post (meaning the "related posts" section of the page can include post excerpts, thumbnail images, or any other content associated with the post).</li></ul><p>Another potential enhancement is to fully automate the process by letting the CMS automatically select related posts for you. In WordPress land there are no doubt plenty of plugins that offer this functionality (but as with all WP plugins, proceed with caution. Here be dragons!). I've used a couple myself over the years, and they've been <em>fine</em>. As far as I can tell they use a combination of metadata matching (are these posts in the same category, for example?) and <a href="https://tomhazledine.com/client-side-search-static-site/#search-fundamentals">"traditional" search queries</a> to compare posts (although how they build their queries I do not know - if I was building a plugin like this I'd probably use some combination of post-title and excerpt to get the best balance of accuracy and performance).</p><h4 id="but-static-sites-dont-use-a-cms">But static sites don't use a CMS...</h4><p>The biggest caveat to this whole approach (and the reason I haven't used it in years) is that a static site doesn't have a CMS. And, in case you missed it, <a href="https://tomhazledine.com/eleventy-static-site-generator">I'm a huge advocate of static sites and static site generators</a>. Sure, some of you might be using a fancy headless CMS or whatever, but for me one of the main appeals of running a static site is that <strong>all my content is nothing more than a folder full of markdown files</strong>.</p><p>The one major downside to <em>just using markdown files</em> is that if you want any custom metadata (such as links to related posts) you have to write it in yourself. The format is a bit more structured than yolo-ing it in the main content flow as-per <a href="#failed-attempt-%231%3A-the-manual-approach">the fully manual approach</a> (if you can call YAML structured #shotsFired), but still comes with all the same downsides.</p><h2 id="so-what-are-embeddings">So what are embeddings?</h2><p>Embeddings power my 100%-automated related posts workflow. The general concept of "embeddings" is an offshoot of the Large Language Model (LLM) technology that makes tools like ChatGPT work. The basic idea is that you can take a piece of text (a blog post, for example) and turn it into a vector (an array of numbers). This vector is called an "embedding" and it represents the "meaning" of the text. It's a weird concept to get your head around at first, but you can learn more about embeddings in detail in Simon Willion's excellent explainer, <a href="https://simonwillison.net/2023/Oct/23/embeddings/">"Embeddings: What they are and why they matter"</a>.</p><p>The way I think about it is like this: an embedding is a mathematical representation of the meaning of a chunk of content. And because the embedding is an array of numbers it can be treated as a set of coordinates. If the embedding was a simple one with only two numbers, you could plot it on a 2D graph and the the position of that point on the graph would represent the meaning of the text used to create the embedding. In essence, <strong>by using embeddings you're creating a "map" of the meaning of your content</strong>.</p><p>Of course it's more complex than that as the embeddings are actually a lot longer than two numbers, but the principle is the same. The longer the vector, the more dimensions you have to plot the point in. The more dimensions you have, the more accurate the representation of the meaning of the text. I've been using OpenAI's <code>ada-002</code> model to create my embeddings, and the embeddings it creates are made up of 1536 numbers. To plot these vectors on a graph you'd need to (somehow) visualise a 1536-dimensional space. Like I said, it's a tricky concept to get your head around.</p><h2 id="using-embeddings-for-natural-language-search">Using embeddings for "natural language search"</h2><p>It is the "semantic mapping" aspect of embeddings that make them useful. If you have a set of embeddings created from different strings of text, their "position" in 1536-dimensional space represents their meaning. If they are close, then the meanings are similar. If they are far apart, then the meanings are different.</p><p>The practical application of this "closeness" is that you can measure how "close" multiple embeddings are and compare them against each other. If one of your strings happens to be a "search query", then tada: you've built a search engine! Rig up an input field to generate an embedding of whatever question the user asks, and then compare that embedding to the embeddings of all your content.</p><p>That simple search engine will work just fine with "normal" queries that you'd type into any old search box, but it would also work really well with <strong>natural language queries</strong>.</p><p>And the similarity is computationally "easy" to calculate, too. If you've already done the work to create the embeddings, then the similarity can be worked out with a function known as <a href="https://en.wikipedia.org/wiki/Cosine_similarity"><strong>cosine similarity</strong></a>.</p><pre><code class="hljs language-js"><span class="hljs-keyword">export</span> <span class="hljs-keyword">const</span> <span class="hljs-title function_">cosineSimilarity</span> = (<span class="hljs-params">a, b</span>) => {
<span class="hljs-keyword">const</span> dotProduct = a.<span class="hljs-title function_">reduce</span>(<span class="hljs-function">(<span class="hljs-params">acc, cur, i</span>) =></span> acc + cur * b[i], <span class="hljs-number">0</span>);
<span class="hljs-keyword">const</span> magnitudeA = <span class="hljs-title class_">Math</span>.<span class="hljs-title function_">sqrt</span>(a.<span class="hljs-title function_">reduce</span>(<span class="hljs-function">(<span class="hljs-params">acc, cur</span>) =></span> acc + cur ** <span class="hljs-number">2</span>, <span class="hljs-number">0</span>));
<span class="hljs-keyword">const</span> magnitudeB = <span class="hljs-title class_">Math</span>.<span class="hljs-title function_">sqrt</span>(b.<span class="hljs-title function_">reduce</span>(<span class="hljs-function">(<span class="hljs-params">acc, cur</span>) =></span> acc + cur ** <span class="hljs-number">2</span>, <span class="hljs-number">0</span>));
<span class="hljs-keyword">const</span> magnitudeProduct = magnitudeA * magnitudeB;
<span class="hljs-keyword">const</span> similarity = dotProduct / magnitudeProduct;
<span class="hljs-keyword">return</span> similarity;
};
</code></pre><p>There's a lot of (to my eyes) complicated maths going on in that <code>cosineSimilarity</code> function, but thankfully you don't need to understand exactly how it works to use it effectively. (Although of course you <em>can</em> just copy/paste that function into <a href="https://chat.openai.com">ChatGPT</a> and get a pretty decent explanation).</p><p>Complex or not, calculating cosine similarity is <strong>a lot</strong> less work than creating a fully-fledged search algorithm, and the results will be of similar quality. In fact, I'd be willing to bet that the embedding-based search would win a head-to-head comparison most of the time.</p><p>There are some caveats to this, as how you've divided up the content before creating your embeddings will effect the results (did you embed every sentence, or every paragraph, or the whole article as a single embedding? Those choices will have consequences).</p><blockquote><h4 id="handy-fact-hallucination-is-not-a-problem-when-working-with-embeddings">Handy fact: hallucination is not a problem when working with embeddings</h4><p>Anyone who's used LLMs for any amount of time will have hit upon the biggest obstacle to using them for "proper work": <strong>LLMs hallucinate</strong>. Yep, they just make stuff up all the time, giving you "facts" that sound plausible but may or may not actually be true. This is an issue with all chat-based LLM interactions where the LLM is generating text.</p><p>The beauty of using LLM embeddings is that at no point is any text being generated. An LLM generates an embedding of text that we explicitly gave it. The "AI magic" is in how it turns the meaning of the text into a list of numbers. We don't need it to generate any text for us, so there's no scope for making stuff up.</p></blockquote><h2 id="can-embeddings-be-used-to-calculate-relations-between-our-content">Can embeddings be used to calculate relations between our content?</h2><p>In short: yes, yes it can. That same <code>cosineSimilarity</code> function that we used to compare a post's embedding to an embedded search term can also be used to compare one post to another. If we do that for all the posts in our blog's archive, then we compare the similarities to find the <code>N</code> most similar posts.</p><p>I've also added some GPT-powered sugar on top of the standard "related posts" concept by getting GPT4 to tell us <em>why</em> the two posts are similar. This lets me flesh out the related-posts section with useful information that will <em>hopefully</em> entice readers into exploring more articles on my site.</p><h2 id="calculating-the-related-posts">Calculating the related posts</h2><p>I've written a node script that does this for my blog posts every time I publish a new article. The script works like this:</p><ol><li>It reads all the markdown (<code>.md</code> or <code>.mdx</code>) files in my content directory, and extracts the relevant content (i.e. the title, subtitle, and body-copy of the post).</li><li>This post content is then sent to the <a href="https://platform.openai.com/docs/api-reference/embeddings/create">OpenAI <strong>embeddings</strong> API</a> to generate a single embedding for that whole post.</li><li>I then use the <a href="https://platform.openai.com/docs/api-reference/completions"><strong>completions</strong> API</a> to summarise the content of each post. This will help me in later steps because the API has limits on how much text it can parse at any one time. Sending over two full blog posts' worth of content will mean I quickly hit the limits and can't generate the "why are these posts similar?" text. By creating a shorter summary of each post at this stage, I can then use those summaries in my later prompts.</li><li>For each post, the script then finds the top two most-similar posts based on the cosine similarity of the embedding vectors.</li><li>For each "similar post" the script then compares the posts' summary with the summary of the starting post, and sends these to GPT4 to generate the "why these are similar" text.</li><li>The final step is to write this "similar posts" data back into the frontmatter of the original markdown files.</li></ol><p>Because this script just needs the markdown files of my blog posts, it can live outside the normal build pipeline of my site. In fact, it could work just as well with any Static Site Generator - if you've got a folder of markdown files, this script would work just fine.</p><h2 id="gotchas">Gotchas</h2><p>Seeing the script laid out step-by-step like that makes it all look rather simple, but there were a few obstacles to overcome that turned an hour's POC into several days' worth of development effort.</p><h3 id="gotcha-1-api-rate-limits">Gotcha #1: API Rate limits</h3><p>It turns out that you can't just spam the OpenAI API with hundreds of requests all at once, so I had to add pauses in my script to account for this. When in "dev mode" the script will stop any wait for user input ("press <code>y</code> to continue") after every API call, but also has a simpler mode for automatically running through all the requests: it just waits for 6 seconds after every API call to before continuing.</p><h3 id="gotcha-2-api-token-limits">Gotcha #2: API token limits</h3><p>As I noted in step #3, the API also has a "context limit" - a.k.a. how much content can it process at once. This is measured in "tokens" (which roughly works out to a token or two per word in a sentence). For GPT3.5 the token limit is 4,097 tokens, and for GPT4 the limit is 8,192.</p><h3 id="gotcha-3-gpt4-pricing">Gotcha #3: GPT4 pricing</h3><p>The OpenAI API is not free to use. Thankfully the embeddings requests are really cheap, and in the process of building and debugging my script I generated literally hundreds of embeddings (using the <code>text-embedding-ada-002</code> model) and never got my usage above even a single dollar. GPT3 (using the <code>gpt-3.5-turbo</code> model) is a little more expensive, and <code>GPT-4</code> is even more so!</p><p>While the cost of generating the embeddings was negligible, generating the <em>"why are these posts similar"</em> text cost about two and half dollars per run of the complete script. This was for my personal blog which has about ~50 posts, and includes generating the summaries and then using those summaries to generate the final text.</p><p>For my own sanity I've spent a lot of time implementing caching to avoid running scripts multiple times for the same content, so future runs (i.e. when new posts are added to my site) will cost a lot less (generally somewhere between a few cents and a dollar).</p><h3 id="gotcha-4-prompt-tweaking">Gotcha #4: Prompt tweaking</h3><p>Again, the embeddings side of things doesn't require any trickery or shenanigans (it just works!), but generating the GPT summaries and <em>"why are these posts similar"</em> content required a bit of what the hype-merchants call "prompt engineering". In my experience, the GPT models can be a bit <em>enthusiastic</em> when generating text, so it took a little bit of careful prompt phrasing to stop them creating over-the-top SEO-fodder (the kind of stuff that marketers and Google seem to love, but real humans hate).</p><p>If you're interested, here's the full prompt I used to generate the <em>"why are these two posts similar"</em> text:</p><pre><code>You are an automatic recommendation engine for my technical blog. At the end of each blog post, you recommend other posts that readers may be interested in.
I'll provide the summaries for two blog posts. Can you describe why someone who has just read the first post might be interested in reading the second post?
Here is the first post:
${postSummary}
Here is the second post:
${similarPostSummary}
Here are some additional instructions to help you write the recommendation:
* Focus on the facts included in the post and the author's opinions.
* Start every summary with "This is similar to what you've just read because"
* Limit responses to single sentence.
* Avoid hyperbole (such as "It's an enlightening read" or similar). Just describe the similarities of the post and why it might be interesting to someone who has just read the first post.
</code></pre><p>There a lot of hand-wavey stuff going on there, but after a lot of experimentation that was the prompt that gave me the most reliable results.</p><h3 id="gotcha-5-inconsistent-output">Gotcha #5: Inconsistent output</h3><p>Annoyingly, even with a super-specific prompt the output from GPT generation is not always consistent. For instance, it <em>sometimes</em> adds quote marks around the final text, and other times it doesn't. Not a huge issue, but something that does need to be accounted for at the code level.</p><h3 id="gotcha-6-hallucination">Gotcha #6: Hallucination</h3><p>Even when mostly relying on hallucination-less embeddings, and even when being super-explicit with my prompts and providing <em>all</em> the relevant content in the API requests, <strong>even then, hallucination is a bit of a problem</strong>. When generating the summaries it can be subtly wrong about the content of the post it's summarising, and when describing why two summaries are similar it occasionally went off-piste and misunderstood the meaning of a post.</p><p>After a lot of prompt-tweaking and experimentation I've ended up with a script that I'm happy with for this specific application, but it does highlight the general problem with hallucination. Now I've spent more time working with these LLMs it's really apparent to me that the hallucinations are <em>the</em> biggest obstacle to doing "useful work" with LLM content generation.</p><h3 id="gotcha-7-post-deletion-requires-recalculating-similarities">Gotcha #7: Post deletion (requires recalculating similarities)</h3><p>Predictably (although I didn't predict it when writing the first iteration of the script!), when a post is deleted or changed you need to re-generate (or at least check) <em>all</em> the similar posts for all of the posts. Easy enough to handle when the content has changed, but deleting a markdown file did result in my script exploding in annoying and hard to debug ways. Got it sorted in the end, but it was enough of a headache to include on this list!</p><h3 id="gotcha-8-node-memory">Gotcha #8: Node memory</h3><p>This was the biggest bit of uncharted territory for me. Storing all that content in Node's memory meant I eventually hit the memory ceiling! Keeping the post content in memory wasn't an issue, and nor was storing the object that contained the summaries and the similarities. The killer (from Node's perspective) was keeping all that <em>and</em> the embeddings (i.e. a whole load of arrays each with 1536 floating points) <em>and</em> not being smart about how many times I map and reduce the data <em>and</em> trying to write them to a file. Probably a problem I would have avoided entirely if I had a CS degree and knew more about what "Big O" meant, but I got there in the end.</p><h2 id="script-optimisation">Script optimisation</h2><p>In the end I did quite a lot of "optimisation" work to get the script to a place where I could rely on it for all the scenarios in my blog workflow (calculating the initial "related posts" for a folder of markdown files, regenerating when new posts are added, handling changes to old post content, and removing and renaming posts).</p><h3 id="caching">Caching</h3><p>To avoid making repeated calls to the (expensive) API for content I'd already generated, I saved the results to a local cache file. If an embedding already existed in my cache, then I could skip fetching that API response a second time. This saved on both execution time and money spent. To invalidate the cache, I generated a <code>sha256</code> hash from the post's content (being sure to hash just the posts' title and content and <em>not</em> the frontmatter, as the script saved it's results in the posts' frontmatter and would therefore instantly invalidate the cache).</p><h3 id="separate-cache-storage-for-embeddings">Separate cache storage for embeddings</h3><p>The embedding arrays, being as long as they are, were a big memory-management concern. In the end, I cached those separately to the rest of the data and just saved a unique key for each embedding in the main cache. That meant that only the embeddings that I actually needed would be loaded into memory.</p><h3 id="regenerate-only-when-content-has-actually-changed">Regenerate only when content has actually changed</h3><p>With the cache/hash pattern in place, the script was able to tell if a post's content had changed since the last time the script had been run. This meant I could skip API calls for any post that hadn't changed, but a subtle "gotcha" was that I still needed to regenerate the related posts for a given post if any of the <em>relations</em> had changed. Thankfully the embedding-based similarity calculation was fast and cheap and could be done without calling any APIs, so I only needed to regenerate the GPT-generated content if the relations had actually changed (I'd re-calculate the relations for every post, but then if the resulting relations were the same for a post, they could be safely skipped).</p><h2 id="ship-it">Ship it!</h2><p>So with the script complete the only thing left to do now is use the thing! The script is triggered by running the command <code>yarn related</code>, and the output looks like this:</p><figure class="post-content__image-wrapper"><img class="post-content__image" src="/images/articles/recent-posts-terminal.png" alt="Sceenshot of terminal output that reads: Generating data for 2023-10-27-llm-related-posts.
Generating summary with 5442 tokens
In this blog post, the author discusses how they used embeddings and GPT4 to generate related posts for their blog. Embeddings, derived from Large Language Model (LLM) technology, are used for ‘natural language search’, determining how similar two pieces of text are. This is useful for search applications and generating related blog posts.
The author provides a step-by-step guide to their automated process for generating related posts. They extract relevant content from all markdown files in their directory, generate a single post embedding with OpenAI's API, find the top two most similar posts, compare these summaries, and writes this data back into the original markdown files.
Several hurdles were encountered along the way, including API rate limits, token limits, pricing, prompt engineering, inconsistency and hallucination in the output, deletion, and node memory. The author overcame these hurdles to use and optimize the script for their blog workflow.
Pausing to allow for openai API rate limiting. Proceed? (y/n)y
Calculating similarities
Comparing 2023-10-27-llm-related-posts to 47 other posts
Generating comparison between 2023-10-27-llm-related-posts & 2021-01-17-adding-rss
This is similar to what you've just read because it also deals with the topic of automated blog content distribution, specifically diving into the process of integrating an RSS feed, a channel which - like the LLM-based automatic posting from the first post - also bypasses algorithmic interferences.
Score: 0.7949880662192309
Generating comparison between 2023-10-27-llm-related-posts & 2022-02-25-wordle-node-script
This is similar to what you've just read because it delves into another practical application of scripting and data manipulation, this time focusing on a word game, which could interest readers who enjoy seeing real-world implications of coding and automated processes.
Score: 0.7938704968029567
Updating frontmatter
Complete!"/><figcaption class="post-content__caption">Screenshot of the terminal output for the script that generates related posts for my blog.</figcaption></figure><p>That output is a little verbose, but it's useful for debugging and for seeing what's going on under the hood. The script also updates the frontmatter of the markdown files with the related posts data, so the final result looks like this:</p><pre><code class="hljs language-yaml"><span class="hljs-attr">related:</span>
<span class="hljs-bullet">-</span> <span class="hljs-attr">relativePath:</span> <span class="hljs-number">2021-01-17</span><span class="hljs-string">-adding-rss</span>
<span class="hljs-attr">permalink:</span> <span class="hljs-string">/adding-rss/</span>
<span class="hljs-attr">date:</span> <span class="hljs-number">2021-01-17</span>
<span class="hljs-attr">tags:</span>
<span class="hljs-bullet">-</span> <span class="hljs-string">articles</span>
<span class="hljs-attr">categories:</span>
<span class="hljs-bullet">-</span> <span class="hljs-string">code</span>
<span class="hljs-attr">title:</span> <span class="hljs-string">RSS</span> <span class="hljs-string">in</span> <span class="hljs-number">2021</span> <span class="hljs-string">(yes,</span> <span class="hljs-string">it's</span> <span class="hljs-string">still</span> <span class="hljs-string">a</span> <span class="hljs-string">thing)</span>
<span class="hljs-attr">excerpt:</span> <span class="hljs-string">Adding</span> <span class="hljs-string">an</span> <span class="hljs-string">RSS</span> <span class="hljs-string">feed</span> <span class="hljs-string">to</span> <span class="hljs-string">an</span> <span class="hljs-string">Eleventy</span> <span class="hljs-string">site</span> <span class="hljs-string">is</span> <span class="hljs-string">(mostly)</span> <span class="hljs-string">easy</span> <span class="hljs-string">peasy.</span>
<span class="hljs-attr">summary:</span>
<span class="hljs-bullet">-</span> <span class="hljs-string">This</span> <span class="hljs-string">is</span> <span class="hljs-string">similar</span> <span class="hljs-string">to</span> <span class="hljs-string">what</span> <span class="hljs-string">you've</span> <span class="hljs-string">just</span> <span class="hljs-string">read</span> <span class="hljs-string">because</span> <span class="hljs-string">it</span> <span class="hljs-string">also</span> <span class="hljs-string">deals</span> <span class="hljs-string">with</span> <span class="hljs-string">the</span>
<span class="hljs-string">topic</span> <span class="hljs-string">of</span> <span class="hljs-string">automated</span> <span class="hljs-string">blog</span> <span class="hljs-string">content</span> <span class="hljs-string">distribution,</span> <span class="hljs-string">specifically</span> <span class="hljs-string">diving</span> <span class="hljs-string">into</span>
<span class="hljs-string">the</span> <span class="hljs-string">process</span> <span class="hljs-string">of</span> <span class="hljs-string">integrating</span> <span class="hljs-string">an</span> <span class="hljs-string">RSS</span> <span class="hljs-string">feed,</span> <span class="hljs-string">a</span> <span class="hljs-string">channel</span> <span class="hljs-string">which</span> <span class="hljs-bullet">-</span> <span class="hljs-string">like</span> <span class="hljs-string">the</span>
<span class="hljs-string">LLM-based</span> <span class="hljs-string">automatic</span> <span class="hljs-string">posting</span> <span class="hljs-string">from</span> <span class="hljs-string">the</span> <span class="hljs-string">first</span> <span class="hljs-string">post</span> <span class="hljs-bullet">-</span> <span class="hljs-string">also</span> <span class="hljs-string">bypasses</span>
<span class="hljs-string">algorithmic</span> <span class="hljs-string">interferences.</span>
<span class="hljs-attr">score:</span> <span class="hljs-number">0.7949880662192309</span>
<span class="hljs-bullet">-</span> <span class="hljs-attr">relativePath:</span> <span class="hljs-number">2022-02-25</span><span class="hljs-string">-wordle-node-script</span>
<span class="hljs-attr">permalink:</span> <span class="hljs-string">/wordle-node-script/</span>
<span class="hljs-attr">date:</span> <span class="hljs-number">2022-02-25</span>
<span class="hljs-attr">tags:</span>
<span class="hljs-bullet">-</span> <span class="hljs-string">articles</span>
<span class="hljs-bullet">-</span> <span class="hljs-string">featured</span>
<span class="hljs-attr">categories:</span>
<span class="hljs-bullet">-</span> <span class="hljs-string">code</span>
<span class="hljs-attr">title:</span> <span class="hljs-string">Improving</span> <span class="hljs-string">my</span> <span class="hljs-string">Wordle</span> <span class="hljs-string">opening</span> <span class="hljs-string">words</span> <span class="hljs-string">using</span> <span class="hljs-string">simple</span> <span class="hljs-string">node</span> <span class="hljs-string">scripts</span>
<span class="hljs-attr">excerpt:</span>
<span class="hljs-string">Crafting</span> <span class="hljs-string">command-line</span> <span class="hljs-string">scripts</span> <span class="hljs-string">to</span> <span class="hljs-string">calculate</span> <span class="hljs-string">the</span> <span class="hljs-string">most</span> <span class="hljs-string">frequently</span> <span class="hljs-string">used</span>
<span class="hljs-string">letters</span> <span class="hljs-string">in</span> <span class="hljs-string">Wordle</span> <span class="hljs-string">(and</span> <span class="hljs-string">finding</span> <span class="hljs-string">an</span> <span class="hljs-string">optimal</span> <span class="hljs-string">sequence</span> <span class="hljs-string">of</span> <span class="hljs-string">starting</span> <span class="hljs-string">words).</span>
<span class="hljs-attr">summary:</span>
<span class="hljs-bullet">-</span> <span class="hljs-string">This</span> <span class="hljs-string">is</span> <span class="hljs-string">similar</span> <span class="hljs-string">to</span> <span class="hljs-string">what</span> <span class="hljs-string">you've</span> <span class="hljs-string">just</span> <span class="hljs-string">read</span> <span class="hljs-string">because</span> <span class="hljs-string">it</span> <span class="hljs-string">delves</span> <span class="hljs-string">into</span> <span class="hljs-string">another</span>
<span class="hljs-string">practical</span> <span class="hljs-string">application</span> <span class="hljs-string">of</span> <span class="hljs-string">scripting</span> <span class="hljs-string">and</span> <span class="hljs-string">data</span> <span class="hljs-string">manipulation,</span> <span class="hljs-string">this</span> <span class="hljs-string">time</span>
<span class="hljs-string">focusing</span> <span class="hljs-string">on</span> <span class="hljs-string">a</span> <span class="hljs-string">word</span> <span class="hljs-string">game,</span> <span class="hljs-string">which</span> <span class="hljs-string">could</span> <span class="hljs-string">interest</span> <span class="hljs-string">readers</span> <span class="hljs-string">who</span> <span class="hljs-string">enjoy</span> <span class="hljs-string">seeing</span>
<span class="hljs-string">real-world</span> <span class="hljs-string">implications</span> <span class="hljs-string">of</span> <span class="hljs-string">coding</span> <span class="hljs-string">and</span> <span class="hljs-string">automated</span> <span class="hljs-string">processes.</span>
<span class="hljs-attr">score:</span> <span class="hljs-number">0.7938704968029567</span>
</code></pre><p>That YAML frontmatter gives me enough content to build a nice little template wrapper for the related posts section of my site. And with all that complete, you should be able to view the real-world results of this script at the bottom of this very page. The "related posts" functionality is live on this blog, and I'm pretty happy with the results. I've been using it for a few weeks now and it's been working well.</p><h3 id="how-much-does-it-cost-to-run">How much does it cost to run?</h3><p>For the record, running that script with a single new blog post resulted in four calls to the GPT4 <code>completions</code> API endpoint, which cost roughly thirty cents (prices in USD as that's what OpenAI use in their billing).</p><h3 id="is-the-script-available-for-anyone-to-use">Is the script available for anyone to use?</h3><p><strong>Update:</strong> Yes, yes it is. I've opend-sourced the script and published it to NPM. You can find it here: <a href="https://github.com/tomhazledine/related-posts">github.com/tomhazledine/related-posts</a>. Currently needs an OpenAI API key to work, but I'm working on a way to hook it up to other LLMs. If you have any questions or suggestions feel free to raise a PR or @ me on Mastodon (I'm <a href="https://mastodon.social/@tomhazledine">@tomhazledine@mastodon.social</a>).</p><p><del>Not yet, but it could be soon. If you're interested in implementing something similar for your own site, I'm not far off packaging the script into something universal that I can open-source on NPM. So if that would be useful to you, then @ me on Mastodon (I'm <a href="https://mastodon.social/@tomhazledine">@tomhazledine@mastodon.social</a>). If more than a couple of people ask me, I'll happilly put in the work to make the script more generic and publish it. It's 90% done already, but as with all software projects I'm anticipating the final 10% of the work will take 90% of the time!</del></p>Improving SVG chart interactivity with Voronoi diagrams2023-09-30T00:00:00Zhttps://tomhazledine.com/improving-svg-chart-interactivity-with-voronoi-diagrams/<p>Scatter plots are a fun way to visualize data, showing both the relationship between two variables and the distribution of data points. And they're reasonably trivial to create in web-friendly SVG (especially if you get a library like <a href="https://d3js.org/">D3.js</a> involved). One issue, though, is hover interactions.</p><p>I've made so many graphs that include hover-triggered tooltips, and the sheer precision required to target those points with a cursor can be infuriating. You can solve this UX problem by making the points (and thus the hover-targets) bigger, but that inteferes with the design of the chart as a whole. For charts with complex datasets, it's often not feasible to make the points big enough to be easily targeted.</p><figure class="post-content__image-wrapper"><div id="voronoi-example-01"><div class="voronoi-scatter__placeholder"/></div><figcaption class="post-content__caption">Tiny points require precision to target with a cursor</figcaption></figure><h2 id="adding-a-hitbox-to-our-points">Adding a hitbox to our points</h2><p>When I'm building charts like these I'm using SVG, so there are plenty of options to try. One approach is to really dive into the "hitbox" analogy from video-games. The visual part of the datapoint doesn't have to be the only part you can target with the mouse!</p><div class="cluster cluster--not-narrow"><blockquote class="width-66"><h3>What is a "hitbox"?</h3><p>In video games, <strong>hitbox</strong> is a term used to describe the area in which a character can be hit by an attack. While a character's appearance can be a complex shape, the hitbox is often much simpler; a primitive circle or rectangle that roughly covers the area of the character.</p><p><strong>Visual size `!==` hitbox size.</strong></p></blockquote><figure class="post-content__image-wrapper width-33"><img src="/images/articles/voronoi-minecraft-hitbox.svg" alt="Diagram of a character with a hitbox that extends beyond the visual portion of the character"/><figcaption class="post-content__caption">Illustration of how a hitbox can extend beyond the visual portion of a character</figcaption></figure></div><h2 id="make-the-logo-hitbox-bigger">Make the <del>logo</del> <em>hitbox</em> bigger</h2><p>With SVG, we can use the <code><g></code> tag to "group" several elements together. This means we can replace a datapoint's single <code><circle></code> element with <em>two</em> circles and treat them as a single element.</p><p>In this example, we have a circle with a radius of <code>2</code> that fires the <code>handleHover()</code> event when hovered. <em>(Note we're assuming the SVG is being writen as a React component in JSX, with <code>d</code> as the data point and <code>handleHover</code> as the event handler.)</em></p><pre><code class="hljs language-jsx"><span class="hljs-keyword">const</span> <span class="hljs-title function_">DataPoint</span> = (<span class="hljs-params">{ d, handleHover }</span>) => (
<span class="xml"><span class="hljs-tag"><<span class="hljs-name">circle</span> <span class="hljs-attr">r</span>=<span class="hljs-string">{2}</span> <span class="hljs-attr">cx</span>=<span class="hljs-string">{d.x}</span> <span class="hljs-attr">cy</span>=<span class="hljs-string">{d.y}</span> <span class="hljs-attr">onMouseOver</span>=<span class="hljs-string">{handleHover}</span> /></span></span>
);
</code></pre><p>To add a bigger hitbox to this point, we can add a second circle with a larger radius and no fill. Because we're adding <code>fill: "none"</code> as a style rule this circle will be invisible, but it will still trigger the <code>handleHover()</code> event when hovered if we set <code>pointerEvents</code> to <code>"all"</code>.</p><pre><code class="hljs language-jsx"><span class="hljs-keyword">const</span> <span class="hljs-title function_">DataPoint</span> = (<span class="hljs-params">{ d, handleHover }</span>) => (
<span class="xml"><span class="hljs-tag"><<span class="hljs-name">g</span>></span>
<span class="hljs-tag"><<span class="hljs-name">circle</span> <span class="hljs-attr">r</span>=<span class="hljs-string">{2}</span> <span class="hljs-attr">cx</span>=<span class="hljs-string">{d.x}</span> <span class="hljs-attr">cy</span>=<span class="hljs-string">{d.y}</span> /></span>
<span class="hljs-tag"><<span class="hljs-name">circle</span>
<span class="hljs-attr">r</span>=<span class="hljs-string">{10}</span>
<span class="hljs-attr">cx</span>=<span class="hljs-string">{d.x}</span>
<span class="hljs-attr">cy</span>=<span class="hljs-string">{d.y}</span>
<span class="hljs-attr">onMouseOver</span>=<span class="hljs-string">{handleHover}</span>
<span class="hljs-attr">style</span>=<span class="hljs-string">{{</span> <span class="hljs-attr">fill:</span> "<span class="hljs-attr">none</span>" }}
<span class="hljs-attr">pointerEvents</span>=<span class="hljs-string">"all"</span>
/></span>
<span class="hljs-tag"></<span class="hljs-name">g</span>></span></span>
);
</code></pre><p>So now we can render a scatter graph with the original design we wanted, but with the added benefit of a larger hitbox for each point.</p><figure class="post-content__image-wrapper"><div id="voronoi-example-02"><div class="voronoi-scatter__placeholder"/></div><figcaption class="post-content__caption">The hitbox is now larger than the visual portion of the datapoint</figcaption></figure><h2 id="more-hitboxes-more-problems">More hitboxes, more problems</h2><p>This approach works well for charts with sparse datasets, but it doesn't scale well. We've now effectively added a buffer zone around each of our points that will obscure anything behind it. This means that if we have a lot of points, we'll end up with a lot of overlapping hitboxes. And overlapping hitboxes means that we could easily be trying to hover over one point, but accidentally trigger the hover event for a different point.</p><p>Ideally we'd like to have hitboxes that are as large as possible <em>without overlapping any other points</em>. What we need, it turns out, is a <strong><a href="https://en.wikipedia.org/wiki/Voronoi_diagram">Voronoi diagram</a></strong>.</p><blockquote><h2 id="what-is-a-voronoi-diagram">What is a Voronoi diagram?</h2><p>At its simplest, a Voronoi diagram divides our chart area into "cells" that are each assigned to a single datapoint. The cell for a given datapoint is the area of the chart that is closer to that datapoint than any other datapoint.</p></blockquote><figure class="post-content__image-wrapper"><div id="voronoi-example-03"><div class="voronoi-scatter__placeholder"/></div><figcaption class="post-content__caption">Example Voronoi diagram</figcaption></figure><p>Voronoi diagrams are named after the Russian mathematician Georgy Voronoy, who introduced the concept in 1908 (although the concept can be traced all with way back to Descartes). Mathematically, the process of creating Voronoi diagrams is a complicated procedure involving <a href="https://en.wikipedia.org/wiki/Delaunay_triangulation">Delaunay triangulation</a> and "circumcircles" (all a mystery to a mere code monkey like myself!), but thankfully we don't have to deal with that directlty when creating our charts in SVG.</p><h2 id="voronoi-diagrams-in-d3js">Voronoi diagrams in D3.js</h2><p>If you're not familiar with D3.js, it's a JavaScript library for creating data visualizations in SVG (it can do <code>canvas</code> stuff too, but all I ever use it for is generating path data to pipe into SVGs). I use it all the time for converting data into charts and graphs.</p><p>It's a powerful library, but it can be a bit daunting at times and a lot of the demo code you can find (which is plentiful and inspiring) assumes a D3-only workflow. My approach is a bit more peacemeal, as I just use the parts of D3.js that I need to generate path data and coordinates that I then "manually" add to SVG in JSX.</p><p>The circles in the example-code above aren't using D3 directly, but my normal workflow is to use D3 to map my data to a pixel domain, so the <code>cx</code> and <code>cy</code> data <em>will</em> have been run through a D3 "scale" function. <em>I'll write more about how I use D3 scales in a future post.</em></p><p>So, to create a Voronoi diagram in D3, we need to use the <code>Delaunay</code> and <code>Voronoi</code> modules. We can then use the <code>Delaunay.from()</code> method to create a Delaunay triangulation from our data, and the <code>voronoi()</code> method to create a Voronoi diagram from that triangulation.</p><pre><code class="hljs language-js"><span class="hljs-keyword">import</span> <span class="hljs-title class_">React</span> <span class="hljs-keyword">from</span> <span class="hljs-string">"react"</span>;
<span class="hljs-keyword">import</span> { <span class="hljs-title class_">Delaunay</span> } <span class="hljs-keyword">from</span> <span class="hljs-string">"d3-delaunay"</span>;
<span class="hljs-keyword">const</span> <span class="hljs-title function_">HoverTargets</span> = (<span class="hljs-params">{ data, scales, layout, handleHover }</span>) => {
<span class="hljs-keyword">const</span> delaunay = <span class="hljs-title class_">Delaunay</span>.<span class="hljs-title function_">from</span>(
data.<span class="hljs-title function_">map</span>(<span class="hljs-function"><span class="hljs-params">d</span> =></span> [scales.<span class="hljs-title function_">x</span>(d.<span class="hljs-property">x</span>), scales.<span class="hljs-title function_">y</span>(d.<span class="hljs-property">y</span>)])
);
<span class="hljs-keyword">const</span> voronoi = delaunay.<span class="hljs-title function_">voronoi</span>([
layout.<span class="hljs-property">graph</span>.<span class="hljs-property">left</span>,
layout.<span class="hljs-property">graph</span>.<span class="hljs-property">top</span>,
layout.<span class="hljs-property">graph</span>.<span class="hljs-property">right</span>,
layout.<span class="hljs-property">graph</span>.<span class="hljs-property">bottom</span>
]);
<span class="hljs-keyword">const</span> shapes = data.<span class="hljs-title function_">map</span>(<span class="hljs-function">(<span class="hljs-params">d, i</span>) =></span> {
<span class="hljs-keyword">const</span> path = voronoi.<span class="hljs-title function_">renderCell</span>(i);
<span class="hljs-keyword">return</span> (
<span class="xml"><span class="hljs-tag"><<span class="hljs-name">path</span>
<span class="hljs-attr">key</span>=<span class="hljs-string">{</span>`<span class="hljs-attr">hover-target-</span>${<span class="hljs-attr">i</span>}`}
<span class="hljs-attr">pointerEvents</span>=<span class="hljs-string">"all"</span>
<span class="hljs-attr">d</span>=<span class="hljs-string">{path}</span>
<span class="hljs-attr">onMouseOver</span>=<span class="hljs-string">{e</span> =></span> handleHover(e, d)}
/></span>
);
});
<span class="hljs-keyword">return</span> <span class="xml"><span class="hljs-tag"><<span class="hljs-name">g</span>></span>{shapes}<span class="hljs-tag"></<span class="hljs-name">g</span>></span></span>;
};
<span class="hljs-keyword">export</span> <span class="hljs-keyword">default</span> <span class="hljs-title class_">HoverTargets</span>;
</code></pre><p>In this example we created a <code>HoverTargets</code> component that will map over our data and return an SVG <code><path></code> for each point (this will be the voronoi "cell"). Within the component we create a Delaunay triangulation object from our data points using the <code>Delaunay.from()</code> method. The <code>data.map(d => [scales.x(d.x), scales.y(d.y)])</code> part of the code uses D3 scales to translate our data points into pixel positions.</p><p>Once we have the Delaunay triangulation, we create a Voronoi diagram by calling <code>delaunay.voronoi()</code> with the boundaries of our graph area as an argument. The result is a Voronoi diagram that perfectly fits within the confines of our chart, and it automatically divides the space into cells that correspond to our data points.</p><p>The we populate our <code><path></code> into the <code>shapes</code> array, with the <code>d</code> value for each path based on the Voronoi cells. When a cell is hovered over, the handleHover function is called, and the associated data point is passed as an argument. This gives us a perfect hitbox for each point.</p><figure class="post-content__image-wrapper"><div id="voronoi-example-04"><div class="voronoi-scatter__placeholder"/></div><figcaption class="post-content__caption">Voronoi diagram incorporated into a scatter plot</figcaption></figure><h2 id="making-it-interactive">Making it interactive</h2><p>The final step is to make the Voronoi diagram interactive by adding an <code>onMouseOver</code> event listener to each Voronoi cell. Then, when the cell is hovered that listener passes the associated data point to the component's state, which we can then use to apply our hover effects to the correct point on the graph. This can include stylistic effects (like visually highlighting the active point, as in the example below) or functional effects like showing a tooltip or adjusting other parts of the graph.</p><figure class="post-content__image-wrapper"><div id="voronoi-example-05"><div class="voronoi-scatter__placeholder"/></div><figcaption class="post-content__caption">Using the Voronoi shapes as hitboxes for the points. The first diagram shows the hitboxes, and the second shows the points with the hitboxes hidden.</figcaption></figure><p>I'll acknowledge that this overview does contain a few <em><a href="https://knowyourmeme.com/memes/how-to-draw-an-owl">"draw the rest of the owl"</a></em> moments. I've assumed a certain amount of familiarity with D3.js and React, but if you already know how to attach event listeners and toggle class names based on state, then this article should give you all you need to get cracing with Voronoi diagrams.</p><p>By using Voronoi diagrams, we can drastically improve the interactivity of SVG charts, making them more user-friendly and accurate. The best part is that it's all possible with just a few lines of additional code when you're already using D3.js. So the next time you find yourself grappling with hover issues on your scatter plot or any other kind of chart, give Voronoi diagrams a try.</p><p>To summarize:</p><ol><li>When building SVG graphs don't rely on hover interactions on visually small elements - they are hard to target with a cursor.</li><li>You can use invisible elements to extend the "hitbox" of your points, but the quick-and-dirty approach (i.e. just makeing the points bigger) can lead to overlapping hitboxes and inaccurate hover interactions.</li><li>Voronoi diagrams are a great way to create hitboxes that are as large as possible without overlapping any other points.</li><li>If you're using D3.js, the <code>d3-delaunay</code> package is a great way to create Voronoi diagrams from your data.</li></ol>What if minesweeper kept getting harder?2023-08-31T00:00:00Zhttps://tomhazledine.com/what-if-minesweeper-kept-getting-harder/<p>I've burnt thousands of hours playing <a href="https://en.wikipedia.org/wiki/Minesweeper_(video_game)">Minesweeper</a> over the years, and my preferred method of keeping things interesting was to add a mine everytime I completed the grid. I often wondered if someone would automate this process, and (as is the way of all software nerds) I inevitably decided to do it myself.</p><blockquote><p><a href="https://incremental-minesweeper.netlify.app/">Play my incrementally harder version here</a></p></blockquote><p><a href="https://incremental-minesweeper.netlify.app/"><img src="/images/articles/minesweeper-screenshot-400.png" alt="Screenshot of my version of Minesweeper, showing a lost game of minesweeper: a 10-by-ten board with 10 mines marked in red in random positions. The player has cleared a portion of the top-right of the board before hitting a mine. They are currently on level 10, and now have the option to restart at level 9. Their high score is shows as being 20."/></a></p><p>This soon turned into <em>quite</em> the project, so I'm explaining myself here. But before I get to that, it's worth explaining how Minesweeper works (for those of you who <em>didn't</em> sink thousands of hours into Windows 3.11/95/98).</p><h2 id="how-to-play-minesweeper">How to play Minesweeper</h2><p>Playing Minesweeper involves clicking cells on a grid to reveal their contents. If you click a cell that contains a mine, you lose. If you clear all the cells that don't contain a mine, you win.</p><p>The basic controls:</p><ul><li>Left-click (or a tap on a touch-enabled device) reveals a cell.</li><li>Right-click (or a long-press) flags<sup id="fnref-1"><a href="#fn-1" class="footnote-ref">1</a></sup> or unflags a cell.</li></ul><p>If the cell you click is <em>adjacent</em> to any mines, it will show you the number of mines that it is adjacent to. For example, if a cell has a <code>1</code> on it, there is 1 mine next to that cell. This could be above, below, left, right, or diagonal to the numbered cell.</p><p>This means you can use logic to deduce where the mines are. For example, if a <code>8</code> is surrounded by eight unrevealed cells you can deduce that <em>every</em> surrounding cell contains a mine.</p><p>If you click a cell that is blank (i.e. contains no number), all the surrounding cells will be revealed. If any of those cells are also blank, all the surrounding cells of those cells will be revealed, and so on. This is called a "cascade" and is a key part of keeping the gameplay speedy.</p><p>Take this example 4x4 grid version of the game...</p><p><img src="/images/articles/minesweeper.svg" alt="3 images of a 4x4 minesweeper board. Board 1 is completely uncleared except for cell B3, which shows a number 1. The second board is a continuation of the game, and shows more cleared cells - with only four left uncleared. Board 3 shows the game in a loss state, with two mines exposed and marked in red."/></p><ol><li>For the first move, the player clicks to reveal cell <strong>B3</strong>. It contains a <code>1</code>, so the player knows that there is one mine adjacent to that cell, but it isn't a particularly useful opening move. There's no clue <em>which</em> of the adjacent cells contains the mine.</li><li>The player then clicks to reveal cell <strong>A3</strong>. This cell is blank, so the "cascade" kicks in and reveals all the connected black cells and the numbered cells that are adjoining. This tells a smart player that cells <strong>C3</strong> and <strong>D3</strong> must contain mines.</li><li>Our player, however, is <em>not</em> smart and clicks on <strong>C3</strong>. This is a mine, and they lose the game.</li></ol><h2 id="how-does-my-version-differ-from-classic-minesweeper">How does my version differ from "classic" Minesweeper?</h2><h3 id="1-difficulty-the-main-point-of-making-my-version">1. Difficulty (the main point of making my version)</h3><p>In my version, the difficulty increases as you play. This is achieved by adding a mine to the grid every time you complete it. This means that the grid gets more difficult to complete as you play, and the number of mines becomes your score.</p><h3 id="2-size">2. Size</h3><p>For my version, I've constrained things to a 10 x 10 grid. The primary reason for this is that I wanted to make the game playable on mobile devices, and ten-by-ten is about the largest I could fit on a phone screen and still be able to play comfortably with my fat and clumsy fingers.</p><h3 id="4-you-always-start-with-a-clear-cell">4. You always start with a clear cell</h3><p>No matter which cell you click first, it will always be a clear cell. This is to avoid the frustration of losing on the first click, which is a common occurrence in the original game.</p><h3 id="5-flagging">5. Flagging</h3><p>At first I left the concept of "flagging" or "marking" cells out of my version, taking the view that "flagging is for chumps". But early playtesters were adamant I add it, so I caved to the pressure.</p><h2 id="how-well-will-you-do">How well will you do?</h2><p>I've been tinkering with this version for about a year now, and my all-time high score is 36. Can you beat that? If so, take a screenshot and show me <a href="https://mastodon.social/@tomhazledine">on Mastodon (@tomhazledine@mastodon.social)</a>.</p><div class="footnotes"><hr/><ol><li id="fn-1"><strong>Note:</strong> "flagging" (or "marking" or "chording" in some versions of the game) is for chumps who aren't confident enough to yolo their way to victory.<a href="#fnref-1" class="footnote-backref">↩</a></li></ol></div>Adding client side search to a static site2023-05-31T00:00:00Zhttps://tomhazledine.com/client-side-search-static-site/<p>In the bad old days (a.k.a. "a couple of years ago") adding search functionality to a statically-generated site required third-party services that provided a search backend (like Algolia or Elastic or whatever). Thankfully we can now run a (simple) search using only client-side tech thanks to libraries like <a href="https://fusejs.io">Fuse.js</a>. As Fuse say in their docs: <em>"you don’t need to setup a dedicated backend just to handle search."</em></p><p>So how does it all work? Why do we even want site search functionality? And what are the steps required to add a decent search experience to a statically-generated site like mine?</p><h2 id="why-add-search-to-a-blog-like-this">Why add search to a blog like this?</h2><p>It's a good question, especially when you consider that Google exists already. But a built-in search function does significantly improve the user experience for a site like this.</p><p>It offers readers a fast and direct way to find the specific content they're interested in, eliminating the need to navigate through multiple pages or understand the overall structure of the site. This ease of use not only enhances the site's accessibility but also increases engagement, as readers are more likely to stay longer and explore more content if they can easily locate the bits that interest them.</p><h2 id="search-fundamentals">Search fundamentals</h2><p>At its simplest, a site-search function is a tool that allows users to search for specific content within a website. When a user enters a search query into the search box on the site, the search function scans the content of the site to find pages that match the query and returns a list of results.</p><p>This is normally a two-step process that consists of "indexing" and "querying".</p><p>To start, an index is created of the content. The index is a representation of all the information on the site that could be relevant to a search and is specifically created to enable efficient searching. The index should contain metadata about each page, such as its title, description, and keywords, as well as the content of the page itself.</p><p>Then, when a search is made, the search engine queries this index to find pages that match the query. The results are then displayed to the user in a list, ordered by relevancy. In the context of a content-search function for a website, the relevance of search results could be determined by factors such as the presence of keywords in the page title, the frequency of keywords in the page content, and the overall quality of the content itself.</p><p>So to build an effective search engine for a site, we need to have two key features:</p><ol><li>A way to create an index of the site's content.</li><li>A way to query that index to return relevant results.</li></ol><h2 id="the-two-options-server-side-and-client-side">The two options: server-side and client-side</h2><p>There are two primary options when it comes to implementing search. The traditional approach is "server-side", where the search is executed on a server. The alternative, which we'll be focusing on, is "client-side", where the search is performed in the user's web browser.</p><p>There are benefits and trade-offs for both approaches, but in my view client-side search is the best option for a site like mine.</p><h2 id="why-not-use-server-side-search">Why not use server-side search?</h2><p>In the past when creating static sites that needed a search function I've used an external service. These have been server-side solutions like Elastic or Algolia, which index your site's content for you and provide API endpoints for performing searches.</p><h3 id="1-cost">1. Cost</h3><p>The biggest obstacle to adding server-side search to a static site is cost. Even though <em>my</em> static site doesn't require a server, if I'm using server-side search there has to be server <em>somewhere</em>. While there are free tiers available for many server-side services, it's likely that I'd need a paid plan. Costs can escalate quickly depending on the amount of data indexed and the number of search queries made, and for a "continual tinkerer" like myself the amount of fine-tuning and customisation I'd want would inevitably result in expensive bills.</p><h3 id="2-fun">2. Fun</h3><p>For a real business deciding between building or buying a complicated second-order function like search, it generally makes sense to let a third party handle the complexity in exchange for money. As highlighted in the point above, I'm not keen on splashing too much cash on this project (aside from ethereal "career" benefits, this blog generates no income at all) but there are other benefits to building it myself.</p><p>For starters, having a proper project to work on is a great way to reinforce things I've learnt. So by building my own search function I'll inevitably learn more about building search functions. Plus I actually enjoy these kinds of nitty-gritty software challenges.</p><p>Building it myself will also give me more control over the end result. I'll be able to fine-tune and customise as much as I like, without having to fight too much against a system that has it's own conventions and defined way of doing things. And that's what my whole career has been built on thus far; learning how to do things myself to get around the limitations of off-the-shelf products.</p><p>But I'm not willing to compromise on the "staticness" of this site, so if I'm determined not to use an external service then I'll have to find an option that works client-side.</p><h2 id="how-does-client-side-search-work">How does client-side search work?</h2><p>For a static content site, client-side search should be a two step process. The first step is to generate an index whenever a site is built. The second step is to provide frontend components to perform a query against that index.</p><p>The index-creation should be handled by the SSG, and the query functionality would be handled by component-level JavaScript.</p><h3 id="creating-the-index">Creating the index</h3><p>Part of the power of using a Static Site Generator is that you can interact with all your content at build time. By generating a search index when the site is built, I'm able to leverage this power to address one of the biggest problems with any client-side search: generating an index is a costly operation.</p><p>To perform a search, the search engine needs an index to query against, and to make an index requires the code to be able to "see" <em>all</em> the content within a site. Server-side search engines traditionally "crawl" all the pages of a website to generate an index that is then stored server-side, but client-side search engines often generate their index in the client at the time a search is made. Using an SSG allows me to get the best of both worlds by generating the index at build time.</p><p>This approach comes with an extra benefit, too: because the index is generated at build time, whenever the site's content changes (and thus triggers a rebuild) the index is automatically regenerated. No more waiting for crawlers or manually-triggered re-indexing. Any search results <em>always</em> represent the most up-to-date content on the site.</p><p>In a client-side context, the best format for a search index is a JSON array. Each page in the site becomes an object in the array, each with a <code>title</code>, <code>url</code>, and <code>content</code> keys. These are the keys I've chosen to use, but the beauty of "rolling your own" is that you can easily tailor this to your own content. Are "tags" crucial to your information architecture? Include them in the search index. The same goes for anything else you think might contribute to the relevancy of search results; categories, keywords, meta-data, etc. If you think it will produce better results, include it in the index (and then test and refine over time).</p><p>Once created, I need a way for my client-side JavaScript to be able to read the index, so I need to make it available to the browser. The easiest way to do this is to save the index as a JSON file and include it in the site's build output. This way the index is available to the browser at the same time as the rest of the site's content. You can see the index for this site here: <a href="https://tomhazledine.com/search-data.json">search-data.json</a></p><pre><code class="hljs language-json"><span class="hljs-punctuation">[</span>
<span class="hljs-punctuation">{</span>
<span class="hljs-attr">"title"</span><span class="hljs-punctuation">:</span> <span class="hljs-string">"Adding client side search to a static site"</span><span class="hljs-punctuation">,</span>
<span class="hljs-attr">"url"</span><span class="hljs-punctuation">:</span> <span class="hljs-string">"/client-side-search-static-site/"</span><span class="hljs-punctuation">,</span>
<span class="hljs-attr">"content"</span><span class="hljs-punctuation">:</span> <span class="hljs-punctuation">[</span>
<span class="hljs-string">"In the bad old days (of, like, a couple of years ago) adding search functionality to a statically-generated site required third-party services that provided a search backend (like Algolia or Elastic or whatever). "</span><span class="hljs-punctuation">,</span>
<span class="hljs-string">"Thankfully we can now run a (simple) search using only client-side tech thanks to libraries like Fuse.js. "</span>
<span class="hljs-comment">// etc...</span>
<span class="hljs-punctuation">]</span>
<span class="hljs-punctuation">}</span><span class="hljs-punctuation">,</span>
<span class="hljs-punctuation">{</span>
<span class="hljs-attr">"title"</span><span class="hljs-punctuation">:</span> <span class="hljs-string">"Oblique Strategies via npx"</span><span class="hljs-punctuation">,</span>
<span class="hljs-attr">"url"</span><span class="hljs-punctuation">:</span> <span class="hljs-string">"/oblique-strategies-via-npx/"</span><span class="hljs-punctuation">,</span>
<span class="hljs-attr">"content"</span><span class="hljs-punctuation">:</span> <span class="hljs-punctuation">[</span>
<span class="hljs-string">"In 1975 Brian Eno and Peter Schmidt released their Oblique Strategies; &quot;Over One Hundred Worthwhile Dilemmas&quot;"</span><span class="hljs-punctuation">,</span>
<span class="hljs-string">"The Oblique Strategies constitute a set of over 100 cards, each of which is a suggestion of a course of action or thinking to assist in creative situations."</span>
<span class="hljs-comment">// etc...</span>
<span class="hljs-punctuation">]</span>
<span class="hljs-punctuation">}</span>
<span class="hljs-comment">// etc...</span>
<span class="hljs-punctuation">]</span>
</code></pre><p><strong>Note:</strong> Because of the way our matching engine works (see the <a href="#fusejs">section on Fuse.js</a> later in this article), the content body is split into an array of sentences. This is achieved using <code>Intl.Segmenter</code>:</p><pre><code class="hljs language-js"><span class="hljs-keyword">const</span> segmenter = <span class="hljs-keyword">new</span> <span class="hljs-title class_">Intl</span>.<span class="hljs-title class_">Segmenter</span>(<span class="hljs-string">"en"</span>, { <span class="hljs-attr">granularity</span>: <span class="hljs-string">"sentence"</span> });
<span class="hljs-keyword">const</span> segments = [...segmenter.<span class="hljs-title function_">segment</span>(content)]
.<span class="hljs-title function_">map</span>(<span class="hljs-function"><span class="hljs-params">segment</span> =></span> segment.<span class="hljs-property">segment</span>)
<span class="hljs-comment">// Cap sentences at 240 chars</span>
.<span class="hljs-title function_">map</span>(<span class="hljs-function"><span class="hljs-params">line</span> =></span> <span class="hljs-title function_">chunk</span>(line, <span class="hljs-number">240</span>))
.<span class="hljs-title function_">flat</span>()
<span class="hljs-comment">// Remove trailing whitespace</span>
.<span class="hljs-title function_">map</span>(<span class="hljs-function"><span class="hljs-params">line</span> =></span> line.<span class="hljs-title function_">trim</span>())
<span class="hljs-comment">// Remove empty lines</span>
.<span class="hljs-title function_">filter</span>(<span class="hljs-function"><span class="hljs-params">line</span> =></span> line.<span class="hljs-property">length</span> > <span class="hljs-number">0</span>);
</code></pre><h3 id="querying-the-index">Querying the index</h3><p>With the index handled, the next step is to write code to query that index. At the core of the search engine is a "matching" function that consumes the index and any given search term and returns a list of results ordered by relevancy.</p><p>A simple "matching" function would search the index for exact matches of a search term, but in practice this does not produce useful results. A more sophisticated matching engine should account for several things:</p><ol><li>It should consider variations of the search term, such as singular versus plural forms or different verb tenses, which can help to find more accurate and relevant results.</li><li>It should account for synonyms or related terms that might be relevant to the search query, allowing for a more comprehensive set of results. For example, a search for "car" might also include results about automobiles, vehicles, or transportation.</li><li>It should account for "stop words", which are common words often excluded from search queries because they are considered to have little semantic value or meaning.</li></ol><p>Stop words are particularly tricky, as sometimes they should be included and sometimes omitted. Stop words are often articles, prepositions, and conjunctions, such as "a", "an", "the", "in", "on", "and", and "or". A naive search function would treat them like any other word in the search query which can lead to search results that are cluttered with irrelevant pages that happen to contain the stop words. For example, if a user searches for "the best coffee in town," a naive search function may return results for every page that includes the words "the", "best", "coffee", and "in" regardless of whether the page is actually about coffee or has any useful information about the best coffee in town.</p><p>A smarter search function would take stop words into account and exclude them from the search query, focusing instead on the more meaningful keywords. But it would also be able to handle cases were users <em>are</em> searching for an exact match. For example, if a user searches for "red shoes" (in quotes), the search engine will only return results that contain that exact phrase, rather than returning any content that contains either "red" or "shoes" separately.</p><p>In short, writing a matching engine is a big project and not one to take lightly. After several POC projects where I compared hand-written options to off-the-shelf options, there was one clear winner: Fuse.js.</p><h2 id="fusejs">Fuse.js</h2><p><a href="https://fusejs.io">Fuse.js</a> is a JavaScript library that focuses on what it calls "fuzzy" searching.</p><blockquote><p>Generally speaking, fuzzy searching (more formally known as approximate string matching) is the technique of finding strings that are approximately equal to a given pattern (rather than exactly).</p></blockquote><p>Fuse performed well in head-to-head tests against other options (including a complex hand-rolled function) and has good documentation, wide usage, and is under active development.</p><p>As with anything code-related, specific implementation details get complicated quickly, but the basic implimentation of Fuse is a straightforward process. We initialise a new instance of Fuse with our index data and a configuration object, and then we can call the <code>search</code> method on that instance with a search term. The <code>search</code> method returns an array of results, each of which contains a reference to the original item in the index and the matched text.</p><pre><code class="hljs language-js"><span class="hljs-keyword">const</span> options = {
<span class="hljs-attr">includeMatches</span>: <span class="hljs-literal">true</span>,
<span class="hljs-attr">findAllMatches</span>: <span class="hljs-literal">true</span>,
<span class="hljs-attr">minMatchCharLength</span>: <span class="hljs-number">2</span>,
<span class="hljs-attr">threshold</span>: <span class="hljs-number">0.3</span>,
<span class="hljs-attr">ignoreLocation</span>: <span class="hljs-literal">true</span>,
<span class="hljs-attr">keys</span>: [
{ <span class="hljs-attr">name</span>: <span class="hljs-string">"title"</span>, <span class="hljs-attr">weight</span>: <span class="hljs-number">2</span> },
{ <span class="hljs-attr">name</span>: <span class="hljs-string">"content"</span>, <span class="hljs-attr">weight</span>: <span class="hljs-number">1</span> }
]
};
<span class="hljs-keyword">const</span> index = <span class="hljs-keyword">await</span> <span class="hljs-title function_">fetch</span>(<span class="hljs-string">"/search-data.json"</span>).<span class="hljs-title function_">then</span>(<span class="hljs-function"><span class="hljs-params">res</span> =></span> res.<span class="hljs-title function_">json</span>());
<span class="hljs-keyword">const</span> fuse = <span class="hljs-keyword">new</span> <span class="hljs-title class_">Fuse</span>(index, options);
<span class="hljs-keyword">const</span> results = fuse.<span class="hljs-title function_">search</span>(<span class="hljs-string">"search term"</span>);
</code></pre><p>The configuration options are powerful and allow for a lot of fine-tuning. The <code>keys</code> option is particularly useful, as it allows us to specify which fields in the index should be searched and how much weight should be given to each field. In the example above, we're telling Fuse to search the <code>title</code> field with twice as much weight as the <code>content</code> field. This means that a match in the <code>title</code> field will be considered twice as relevant as a match in the <code>content</code> field.</p><p>I'm also setting the threshold to <code>0.3</code> (meaning that only results with a score of <code>0.3</code> or higher will be returned) which is a comparatively low value. My site doesn't have thousands of pages, so I'm happy to return a lot of results and let the user decide which is most relevant. If you have a larger site, you may want to increase this value to return fewer results.</p><h2 id="optimization">Optimization</h2><p>Ultimately there are some trade-offs and compromises that come from choosing client-side over server-side search. These are few, thankfully, and I've mitigated as many of these as I feasibly can.</p><p>While the configuration options are powerful, they are not as powerful as a fully server-side stack and Elastic (or other similar services). A client-side option is always going to be constrained by the power of the browser it's running inside.</p><p>For deep sites, the index can get quite large (sometimes up to multiple megabytes for sites with thousands of pages). For the search to work that index needs to be loaded into the client. I've mitigated this by lazy-loading the index and leveraging caching, but there's no way to <em>search</em> an index without <em>having</em> the index. This is one area where server-side power can really speed things up. But on the flip-side, a server-side search will always require a network round-trip to fetch the results, so while the client-side search is slower on first load, every actual search feels pleasingly fast.</p><h2 id="conclusion">Conclusion</h2><p>I'm really happy with the search experience on my site. It's fast, it's accurate, and it's easy to use. It's also a great example of how powerful modern browsers are and how much can be achieved with a little bit of JavaScript.</p><p>And because I've built it myself, I can tweak and improve it as I see fit. I can add new features, change the UI, and make it work exactly how I want it to. I can also use it as a learning tool to better understand how search engines work and how to improve them.</p>Oblique Strategies via npx2022-10-24T00:00:00Zhttps://tomhazledine.com/oblique-strategies-via-npx/<p>In 1975 Brian Eno and Peter Schmidt released their <a href="https://www.enoshop.co.uk/product/oblique-strategies.html">Oblique Strategies</a>; <em>"Over One Hundred Worthwhile Dilemmas"</em></p>
<blockquote>
<p>The Oblique Strategies constitute a set of over 100 cards, each of which is a suggestion of a course of action or thinking to assist in creative situations. These famous cards have been used by many artists and creative people all over the world since their initial publication. Fifth edition 2001.</p>
</blockquote>
<p>You can buy your own set of the cards on Brian Eno's shop (<a href="https://www.enoshop.co.uk/product/oblique-strategies.html">www.enoshop.co.uk/product/oblique-strategies</a>), but you can also get a mini taste of oblique inspiration in your terminal by running the following npx command:</p>
<pre><code class="language-bash">npx oblique-strategy
</code></pre>
<h2 id="examples" tabindex="-1">Examples</h2>
<pre><code>┌────────────────────────┐
│ │
│ Remove a restriction │
│ │
│ - Edition 4 (1996) │
│ │
└────────────────────────┘
</code></pre>
<pre><code>┌─────────────────────────────┐
│ │
│ Is the tuning appropriate │
│ │
│ - Edition 1 (1975) │
│ │
└─────────────────────────────┘
</code></pre>
<pre><code>┌──────────────────────────────────┐
│ │
│ Fill every beat with something │
│ │
│ - Edition 2 (1978) │
│ │
└──────────────────────────────────┘
</code></pre>
<h2 id="about-the-code" tabindex="-1">About the code</h2>
<p>The full code for this npm package is viewable on my GitHub at <a href="https://github.com/tomhazledine/oblique-strategy">github.com/tomhazledine/oblique-strategy</a>, but there's not really too much to it. There are more lines dedicated to drawing the box around the output than there are for actually picking the strategy.</p>
<figure><img src="/images/articles/oblique-strategies-box-600.jpg" alt="" data-alt="An open box of Oblique Strategies on a wooden table-top. The top card is visible, and reads 'Think: Inside the work. Outside the work.'"/><figcaption>"proof" that I own a legit copy of the strategies</figcaption></figure>
<p>The strategies themselves were all found on other GitHub repos. I've deliberately not credited any of those projects because, well, they're clearly breaching some kind of copyright 🤷♂️ - as too, I should note, am I. As recompense for my flagrant disregard of others' work, I've bought a physical copy of the Oblique Strategies <a href="https://www.enoshop.co.uk/product/oblique-strategies.html">for the princely sum of £50</a> and I'll take this repo down the second anyone complains.</p>
<p>In truth this was more of a learning exercise for me. I wanted to master the <code>npx</code> process and learned a lot about <code>package.bin</code> in the process, so I'm considering this project a success.</p>Improving my Wordle opening words using simple node scripts2022-02-25T00:00:00Zhttps://tomhazledine.com/wordle-node-script/<p>Let’s be honest, by this point you’re either addicted to <a href="https://www.powerlanguage.co.uk/wordle/">Wordle</a><sup id="fnref-1"><a href="#fn-1" class="footnote-ref">1</a></sup> or sick to death of hearing about it. And yes, I'm shamelessly using Wordle as click-bait for this post. Having lured you in with the promise of Wordle-insight (which I will deliver!) I'm going to try and get you excited about writing tiny scripts.</p><p>Tiny scripts that you run in your terminal can help with, well, pretty much anything involving large amounts of data. They can help you with the stuff that humans are generally bad at but computers are great at. Like analysing the 12,972 words that Wordle uses in its code.</p><h2 id="i-want-to-find-a-sequence-of-guesses-that-covers-as-many-of-the-most-common-consonants-as-possible">I want to find a sequence of guesses that covers as many of the most common consonants as possible.</h2><p>My strategy is to hit the most common letters as quickly as possible, but at the same time be able to get through as many guesses as possible without repeating letters. I’m not “solving” Wordle; I just want to optimise my opening sequence of guesses to make playing it more fun.<sup id="fnref-2"><a href="#fn-2" class="footnote-ref">2</a></sup></p><p>The practical considerations of my approach:</p><ol><li>Try to minimise the amount of vowels in the first couple of guesses. Once you get past guess #3 words are harder to form without repeating letters, so keeping some handy word-makers in my pocket is helpful.</li><li>It would be really helpful to know which letters actually <em>are</em> the most common (rather than just relying on my flawed gut-feel).</li><li>I need a "complete" dictionary of words to choose from (a.k.a. my own vocabulary is not good enough).</li></ol><p>I can solve all three issues by writing a little bit of code that I'll run on the command-line in my terminal app.</p><h2 id="scripts-can-be-gross-and-bad-as-long-as-they-work">Scripts can be gross and "bad", as long as they work</h2><p>One joy of tiny scripts is that they're just for me and all I really care about is the <em>output</em>. That means I can use whatever language I like. I spend most of my time writing JavaScript so I'll use Node for my scripts, but you can 100% get the same results with Python or PHP or bash or whatever you like.</p><p>It also means that I can play fast and loose with "best practices". Working something out for thousands of items (in this case the Wordle words) is practically impossible by hand, but easy for a script, so even if my code is objectively terrible (with multiple inefficiencies and <code>O(n log n)</code> complexity), the computer can still calculate the result in fractions of a second.</p><p>I also don't need to worry about extensibility or re-use. As long as it works for the data-set I'm working with right now, that's fine! I'll just throw this script away when I'm done anyway.</p><h2 id="where-does-wordle-get-its-words-from">Where does Wordle get its words from?</h2><p>The source code of Wordle<sup id="fnref-3"><a href="#fn-3" class="footnote-ref">3</a></sup> contains two arrays of words which I'm calling "winners" and "guesses". <code>winners</code> has 2,315 items and shows all the potential winning words, whereas <code>guesses</code> is longer (10,657) and contains all possible valid guesses.</p><pre><code class="hljs language-js"><span class="hljs-keyword">export</span> <span class="hljs-keyword">const</span> winners = [<span class="hljs-string">"cigar"</span>, <span class="hljs-string">"rebut"</span>, <span class="hljs-string">"sissy"</span>, <span class="hljs-string">"humph"</span>, <span class="hljs-string">"awake"</span>, <span class="hljs-comment">/* etc. */</span> ],
<span class="hljs-keyword">export</span> <span class="hljs-keyword">const</span> guesses = [<span class="hljs-string">"aahed"</span>, <span class="hljs-string">"aalii"</span>, <span class="hljs-string">"aargh"</span> <span class="hljs-comment">/* etc. */</span>];
<span class="hljs-comment">// Combine the two lists into one array</span>
<span class="hljs-keyword">export</span> <span class="hljs-keyword">const</span> words = [...winners,...guesses];
</code></pre><figcaption>I've saved these lists in a js file called `/dictionary.js`</figcaption><h2 id="which-letters-are-most-common">Which letters are most common?</h2><p>Working out the frequency of letters is a three-step process. The first step is to count how many times each letter appears in the list of words. Then I need to sort those letters into a useful order (most frequent at the start, least frequent at the end). The third step is to work out the <em>relative</em> frequency of each letter from the raw count.</p><p>The initial counting is done by importing our word list<sup id="fnref-4"><a href="#fn-4" class="footnote-ref">4</a></sup> and flattening it to a single string of letters with <code>words.join('')</code>. We then turn this into an array of letters with a "spread": <code>[...words.join('')]</code>. The we run that array through a <code>reduce()</code> operation to create an object where every letter is a key with a the letter's count as a value.</p><pre><code class="hljs language-js"><span class="hljs-keyword">import</span> { words } <span class="hljs-keyword">from</span> <span class="hljs-string">"./dictionary.js"</span>;
<span class="hljs-keyword">const</span> <span class="hljs-title function_">letterFrequency</span> = letters =>
letters.<span class="hljs-title function_">reduce</span>(<span class="hljs-function">(<span class="hljs-params">total, letter</span>) =></span> {
total[letter] ? total[letter]++ : (total[letter] = <span class="hljs-number">1</span>);
<span class="hljs-keyword">return</span> total;
}, {});
<span class="hljs-keyword">const</span> allLetters = [...words.<span class="hljs-title function_">join</span>(<span class="hljs-string">""</span>)];
<span class="hljs-keyword">const</span> unsortedFrequencies = <span class="hljs-title function_">letterFrequency</span>(allLetters);
</code></pre><p>That <code>letterFrequency()</code> function produces an output that looks like this:</p><pre><code class="hljs language-js"><span class="hljs-attr">unsortedFrequencies</span>: {
<span class="hljs-attr">h</span>: <span class="hljs-number">1371</span>,
<span class="hljs-attr">d</span>: <span class="hljs-number">2060</span>,
<span class="hljs-attr">l</span>: <span class="hljs-number">2652</span>,
<span class="hljs-comment">// etc...</span>
}
</code></pre><p>Sorting is a bit fiddly. Because our unsorted letters are in object-form, we'll need to use <code>Object.keys(frequencies)</code> to allow us to map through the keys and apply a <code>.sort()</code> to them.</p><pre><code class="hljs language-js"><span class="hljs-keyword">const</span> <span class="hljs-title function_">sortFrequencies</span> = frequencies =>
<span class="hljs-title class_">Object</span>.<span class="hljs-title function_">keys</span>(frequencies)
.<span class="hljs-title function_">map</span>(<span class="hljs-function"><span class="hljs-params">key</span> =></span> ({
<span class="hljs-attr">letter</span>: key,
<span class="hljs-attr">count</span>: frequencies[key]
}))
.<span class="hljs-title function_">sort</span>(<span class="hljs-function">(<span class="hljs-params">a, b</span>) =></span> frequencies[b] - frequencies[a]);
<span class="hljs-keyword">const</span> sortedFrequencies = <span class="hljs-title function_">sortFrequencies</span>(unsortedFrequencies);
</code></pre><p>The third step is to calculate the percentage that each letter occurs in the full list of letters. I'm aiming for accuracy to two decimal places here, hence the <code>* 100</code> and <code>/ 100</code> dance I need to do to get the rounding to work.</p><pre><code class="hljs language-js"><span class="hljs-keyword">const</span> <span class="hljs-title function_">calculatePercentages</span> = (<span class="hljs-params">totalCount, frequencies</span>) =>
frequencies.<span class="hljs-title function_">map</span>(<span class="hljs-function"><span class="hljs-params">frequency</span> =></span> {
<span class="hljs-keyword">const</span> percentage =
<span class="hljs-title class_">Math</span>.<span class="hljs-title function_">round</span>((frequency.<span class="hljs-property">count</span> / totalCount) * <span class="hljs-number">100</span>) / <span class="hljs-number">100</span>;
<span class="hljs-keyword">return</span> { ...frequency, percentage };
});
<span class="hljs-keyword">const</span> percentages = <span class="hljs-title function_">calculatePercentages</span>(allLetters.<span class="hljs-property">length</span>, sortedFrequencies);
</code></pre><h2 id="letter-frequency-results">Letter frequency results</h2><p>So what does this little script discover? I've saved it in a file called <code>frequencies.js</code>, so to run it I open my terminal, <code>cd</code> to the directory that contains the script, and run:</p><pre><code>node frequencies
</code></pre><p>For my real version I "wasted" some time building a little function to print the results as a graph directly into my terminal (see the screenshot), but for this post I've prettied-up the results a little more.</p><p><img src="/images/articles/wordle-script-screenshot.png" alt="A screenshot of my terminal showing the output of my letter-frequency calculations as a bar chart, replicated on this page in more accessible form below"/></p><p>My first run of the script found the most common letters in the entire Wordle "dictionary". Unsurprisingly the vowels all ranked pretty high, but the most commonly occurring letter was <code>S</code>.</p><figure><div class="bar-graph"><div class="bar-graph__tick-labels"><span class="bar-graph__tick-label" style="left:20%">5%</span><span class="bar-graph__tick-label" style="left:40%">10%</span><span class="bar-graph__tick-label" style="left:60%">15%</span><span class="bar-graph__tick-label" style="left:80%">20%</span></div><div class="bar-graph__bar-wrapper"><span class="bar-graph__bar-index">01:</span><span class="bar-graph__bar-key">S</span><div class="bar-graph__bar"><span class="bar-graph__bar-value" style="width:40%"/><div class="bar-graph__ticks"><span class="bar-graph__tick" style="left:20%"/><span class="bar-graph__tick" style="left:40%"/><span class="bar-graph__tick" style="left:60%"/><span class="bar-graph__tick" style="left:80%"/></div><span class="bar-graph__bar-label" style="left:40%">10% (6665)</span></div></div><div class="bar-graph__bar-wrapper"><span class="bar-graph__bar-index">02:</span><span class="bar-graph__bar-key">E</span><div class="bar-graph__bar"><span class="bar-graph__bar-value" style="width:40%"/><div class="bar-graph__ticks"><span class="bar-graph__tick" style="left:20%"/><span class="bar-graph__tick" style="left:40%"/><span class="bar-graph__tick" style="left:60%"/><span class="bar-graph__tick" style="left:80%"/></div><span class="bar-graph__bar-label" style="left:40%">10% (6662)</span></div></div><div class="bar-graph__bar-wrapper"><span class="bar-graph__bar-index">03:</span><span class="bar-graph__bar-key">A</span><div class="bar-graph__bar"><span class="bar-graph__bar-value" style="width:36%"/><div class="bar-graph__ticks"><span class="bar-graph__tick" style="left:20%"/><span class="bar-graph__tick" style="left:40%"/><span class="bar-graph__tick" style="left:60%"/><span class="bar-graph__tick" style="left:80%"/></div><span class="bar-graph__bar-label" style="left:36%">9% (5990)</span></div></div><div class="bar-graph__bar-wrapper"><span class="bar-graph__bar-index">04:</span><span class="bar-graph__bar-key">O</span><div class="bar-graph__bar"><span class="bar-graph__bar-value" style="width:28%"/><div class="bar-graph__ticks"><span class="bar-graph__tick" style="left:20%"/><span class="bar-graph__tick" style="left:40%"/><span class="bar-graph__tick" style="left:60%"/><span class="bar-graph__tick" style="left:80%"/></div><span class="bar-graph__bar-label" style="left:28%">7% (4438)</span></div></div><div class="bar-graph__bar-wrapper"><span class="bar-graph__bar-index">05:</span><span class="bar-graph__bar-key">R</span><div class="bar-graph__bar"><span class="bar-graph__bar-value" style="width:24%"/><div class="bar-graph__ticks"><span class="bar-graph__tick" style="left:20%"/><span class="bar-graph__tick" style="left:40%"/><span class="bar-graph__tick" style="left:60%"/><span class="bar-graph__tick" style="left:80%"/></div><span class="bar-graph__bar-label" style="left:24%">6% (4158)</span></div></div><div class="bar-graph__bar-wrapper"><span class="bar-graph__bar-index">06:</span><span class="bar-graph__bar-key">I</span><div class="bar-graph__bar"><span class="bar-graph__bar-value" style="width:24%"/><div class="bar-graph__ticks"><span class="bar-graph__tick" style="left:20%"/><span class="bar-graph__tick" style="left:40%"/><span class="bar-graph__tick" style="left:60%"/><span class="bar-graph__tick" style="left:80%"/></div><span class="bar-graph__bar-label" style="left:24%">6% (3759)</span></div></div><div class="bar-graph__bar-wrapper"><span class="bar-graph__bar-index">07:</span><span class="bar-graph__bar-key">L</span><div class="bar-graph__bar"><span class="bar-graph__bar-value" style="width:20%"/><div class="bar-graph__ticks"><span class="bar-graph__tick" style="left:20%"/><span class="bar-graph__tick" style="left:40%"/><span class="bar-graph__tick" style="left:60%"/><span class="bar-graph__tick" style="left:80%"/></div><span class="bar-graph__bar-label" style="left:20%">5% (3371)</span></div></div><div class="bar-graph__bar-wrapper"><span class="bar-graph__bar-index">08:</span><span class="bar-graph__bar-key">T</span><div class="bar-graph__bar"><span class="bar-graph__bar-value" style="width:20%"/><div class="bar-graph__ticks"><span class="bar-graph__tick" style="left:20%"/><span class="bar-graph__tick" style="left:40%"/><span class="bar-graph__tick" style="left:60%"/><span class="bar-graph__tick" style="left:80%"/></div><span class="bar-graph__bar-label" style="left:20%">5% (3295)</span></div></div><div class="bar-graph__bar-wrapper"><span class="bar-graph__bar-index">09:</span><span class="bar-graph__bar-key">N</span><div class="bar-graph__bar"><span class="bar-graph__bar-value" style="width:20%"/><div class="bar-graph__ticks"><span class="bar-graph__tick" style="left:20%"/><span class="bar-graph__tick" style="left:40%"/><span class="bar-graph__tick" style="left:60%"/><span class="bar-graph__tick" style="left:80%"/></div><span class="bar-graph__bar-label" style="left:20%">5% (2952)</span></div></div><div class="bar-graph__bar-wrapper"><span class="bar-graph__bar-index">10:</span><span class="bar-graph__bar-key">U</span><div class="bar-graph__bar"><span class="bar-graph__bar-value" style="width:16%"/><div class="bar-graph__ticks"><span class="bar-graph__tick" style="left:20%"/><span class="bar-graph__tick" style="left:40%"/><span class="bar-graph__tick" style="left:60%"/><span class="bar-graph__tick" style="left:80%"/></div><span class="bar-graph__bar-label" style="left:16%">4% (2511)</span></div></div></div><figcaption>The 10 most common letters in the Wordle dictionary</figcaption></figure><p>For my second run of the script, I filtered out the vowels. This doesn't change the values for any of the letters, but lets me focus on the letters that I think might be more "valuable" when choosing Wordle guesses.</p><figure><div class="bar-graph"><div class="bar-graph__tick-labels"><span class="bar-graph__tick-label" style="left:20%">5%</span><span class="bar-graph__tick-label" style="left:40%">10%</span><span class="bar-graph__tick-label" style="left:60%">15%</span><span class="bar-graph__tick-label" style="left:80%">20%</span></div><div class="bar-graph__bar-wrapper"><span class="bar-graph__bar-index">01:</span><span class="bar-graph__bar-key">S</span><div class="bar-graph__bar"><span class="bar-graph__bar-value" style="width:40%"/><div class="bar-graph__ticks"><span class="bar-graph__tick" style="left:20%"/><span class="bar-graph__tick" style="left:40%"/><span class="bar-graph__tick" style="left:60%"/><span class="bar-graph__tick" style="left:80%"/></div><span class="bar-graph__bar-label" style="left:40%">10% (6665)</span></div></div><div class="bar-graph__bar-wrapper"><span class="bar-graph__bar-index">02:</span><span class="bar-graph__bar-key">R</span><div class="bar-graph__bar"><span class="bar-graph__bar-value" style="width:24%"/><div class="bar-graph__ticks"><span class="bar-graph__tick" style="left:20%"/><span class="bar-graph__tick" style="left:40%"/><span class="bar-graph__tick" style="left:60%"/><span class="bar-graph__tick" style="left:80%"/></div><span class="bar-graph__bar-label" style="left:24%">6% (4158)</span></div></div><div class="bar-graph__bar-wrapper"><span class="bar-graph__bar-index">03:</span><span class="bar-graph__bar-key">L</span><div class="bar-graph__bar"><span class="bar-graph__bar-value" style="width:20%"/><div class="bar-graph__ticks"><span class="bar-graph__tick" style="left:20%"/><span class="bar-graph__tick" style="left:40%"/><span class="bar-graph__tick" style="left:60%"/><span class="bar-graph__tick" style="left:80%"/></div><span class="bar-graph__bar-label" style="left:20%">5% (3371)</span></div></div><div class="bar-graph__bar-wrapper"><span class="bar-graph__bar-index">04:</span><span class="bar-graph__bar-key">T</span><div class="bar-graph__bar"><span class="bar-graph__bar-value" style="width:20%"/><div class="bar-graph__ticks"><span class="bar-graph__tick" style="left:20%"/><span class="bar-graph__tick" style="left:40%"/><span class="bar-graph__tick" style="left:60%"/><span class="bar-graph__tick" style="left:80%"/></div><span class="bar-graph__bar-label" style="left:20%">5% (3295)</span></div></div><div class="bar-graph__bar-wrapper"><span class="bar-graph__bar-index">05:</span><span class="bar-graph__bar-key">N</span><div class="bar-graph__bar"><span class="bar-graph__bar-value" style="width:20%"/><div class="bar-graph__ticks"><span class="bar-graph__tick" style="left:20%"/><span class="bar-graph__tick" style="left:40%"/><span class="bar-graph__tick" style="left:60%"/><span class="bar-graph__tick" style="left:80%"/></div><span class="bar-graph__bar-label" style="left:20%">5% (2952)</span></div></div><div class="bar-graph__bar-wrapper"><span class="bar-graph__bar-index">06:</span><span class="bar-graph__bar-key">D</span><div class="bar-graph__bar"><span class="bar-graph__bar-value" style="width:16%"/><div class="bar-graph__ticks"><span class="bar-graph__tick" style="left:20%"/><span class="bar-graph__tick" style="left:40%"/><span class="bar-graph__tick" style="left:60%"/><span class="bar-graph__tick" style="left:80%"/></div><span class="bar-graph__bar-label" style="left:16%">4% (2453)</span></div></div><div class="bar-graph__bar-wrapper"><span class="bar-graph__bar-index">07:</span><span class="bar-graph__bar-key">Y</span><div class="bar-graph__bar"><span class="bar-graph__bar-value" style="width:12%"/><div class="bar-graph__ticks"><span class="bar-graph__tick" style="left:20%"/><span class="bar-graph__tick" style="left:40%"/><span class="bar-graph__tick" style="left:60%"/><span class="bar-graph__tick" style="left:80%"/></div><span class="bar-graph__bar-label" style="left:12%">3% (2074)</span></div></div><div class="bar-graph__bar-wrapper"><span class="bar-graph__bar-index">08:</span><span class="bar-graph__bar-key">C</span><div class="bar-graph__bar"><span class="bar-graph__bar-value" style="width:12%"/><div class="bar-graph__ticks"><span class="bar-graph__tick" style="left:20%"/><span class="bar-graph__tick" style="left:40%"/><span class="bar-graph__tick" style="left:60%"/><span class="bar-graph__tick" style="left:80%"/></div><span class="bar-graph__bar-label" style="left:12%">3% (2028)</span></div></div><div class="bar-graph__bar-wrapper"><span class="bar-graph__bar-index">09:</span><span class="bar-graph__bar-key">P</span><div class="bar-graph__bar"><span class="bar-graph__bar-value" style="width:12%"/><div class="bar-graph__ticks"><span class="bar-graph__tick" style="left:20%"/><span class="bar-graph__tick" style="left:40%"/><span class="bar-graph__tick" style="left:60%"/><span class="bar-graph__tick" style="left:80%"/></div><span class="bar-graph__bar-label" style="left:12%">3% (2019)</span></div></div><div class="bar-graph__bar-wrapper"><span class="bar-graph__bar-index">10:</span><span class="bar-graph__bar-key">M</span><div class="bar-graph__bar"><span class="bar-graph__bar-value" style="width:12%"/><div class="bar-graph__ticks"><span class="bar-graph__tick" style="left:20%"/><span class="bar-graph__tick" style="left:40%"/><span class="bar-graph__tick" style="left:60%"/><span class="bar-graph__tick" style="left:80%"/></div><span class="bar-graph__bar-label" style="left:12%">3% (1976)</span></div></div></div><figcaption>The 10 most common consonants in the Wordle dictionary</figcaption></figure><p>For completeness, and to assuage my curiosity, I also ran the script against just the "winning" words. This one is potentially a "spoiler" and some could call it cheating, so feel free to skip these results if you like. It was interesting, however, to see how the frequencies changed for this reduced set of words!</p><div class="js__toggleVisibility"><button class="spoiler__toggle button js__showHideToggle showHide__toggle">the spoiler</button><div class="spoiler js__showHideArea showHide__area"><div class="js__showHideAreaInner showHide__inner"><figure><div class="bar-graph"><div class="bar-graph__tick-labels"><span class="bar-graph__tick-label" style="left:20%">5%</span><span class="bar-graph__tick-label" style="left:40%">10%</span><span class="bar-graph__tick-label" style="left:60%">15%</span><span class="bar-graph__tick-label" style="left:80%">20%</span></div><div class="bar-graph__bar-wrapper"><span class="bar-graph__bar-index">01:</span><span class="bar-graph__bar-key">E</span><div class="bar-graph__bar"><span class="bar-graph__bar-value" style="width:44%"/><div class="bar-graph__ticks"><span class="bar-graph__tick" style="left:20%"/><span class="bar-graph__tick" style="left:40%"/><span class="bar-graph__tick" style="left:60%"/><span class="bar-graph__tick" style="left:80%"/></div><span class="bar-graph__bar-label" style="left:44%">11% (1233)</span></div></div><div class="bar-graph__bar-wrapper"><span class="bar-graph__bar-index">02:</span><span class="bar-graph__bar-key">A</span><div class="bar-graph__bar"><span class="bar-graph__bar-value" style="width:32%"/><div class="bar-graph__ticks"><span class="bar-graph__tick" style="left:20%"/><span class="bar-graph__tick" style="left:40%"/><span class="bar-graph__tick" style="left:60%"/><span class="bar-graph__tick" style="left:80%"/></div><span class="bar-graph__bar-label" style="left:32%">8% (979)</span></div></div><div class="bar-graph__bar-wrapper"><span class="bar-graph__bar-index">03:</span><span class="bar-graph__bar-key">R</span><div class="bar-graph__bar"><span class="bar-graph__bar-value" style="width:32%"/><div class="bar-graph__ticks"><span class="bar-graph__tick" style="left:20%"/><span class="bar-graph__tick" style="left:40%"/><span class="bar-graph__tick" style="left:60%"/><span class="bar-graph__tick" style="left:80%"/></div><span class="bar-graph__bar-label" style="left:32%">8% (899)</span></div></div><div class="bar-graph__bar-wrapper"><span class="bar-graph__bar-index">04:</span><span class="bar-graph__bar-key">O</span><div class="bar-graph__bar"><span class="bar-graph__bar-value" style="width:28%"/><div class="bar-graph__ticks"><span class="bar-graph__tick" style="left:20%"/><span class="bar-graph__tick" style="left:40%"/><span class="bar-graph__tick" style="left:60%"/><span class="bar-graph__tick" style="left:80%"/></div><span class="bar-graph__bar-label" style="left:28%">7% (754)</span></div></div><div class="bar-graph__bar-wrapper"><span class="bar-graph__bar-index">05:</span><span class="bar-graph__bar-key">T</span><div class="bar-graph__bar"><span class="bar-graph__bar-value" style="width:24%"/><div class="bar-graph__ticks"><span class="bar-graph__tick" style="left:20%"/><span class="bar-graph__tick" style="left:40%"/><span class="bar-graph__tick" style="left:60%"/><span class="bar-graph__tick" style="left:80%"/></div><span class="bar-graph__bar-label" style="left:24%">6% (729)</span></div></div><div class="bar-graph__bar-wrapper"><span class="bar-graph__bar-index">06:</span><span class="bar-graph__bar-key">L</span><div class="bar-graph__bar"><span class="bar-graph__bar-value" style="width:24%"/><div class="bar-graph__ticks"><span class="bar-graph__tick" style="left:20%"/><span class="bar-graph__tick" style="left:40%"/><span class="bar-graph__tick" style="left:60%"/><span class="bar-graph__tick" style="left:80%"/></div><span class="bar-graph__bar-label" style="left:24%">6% (719)</span></div></div><div class="bar-graph__bar-wrapper"><span class="bar-graph__bar-index">07:</span><span class="bar-graph__bar-key">I</span><div class="bar-graph__bar"><span class="bar-graph__bar-value" style="width:24%"/><div class="bar-graph__ticks"><span class="bar-graph__tick" style="left:20%"/><span class="bar-graph__tick" style="left:40%"/><span class="bar-graph__tick" style="left:60%"/><span class="bar-graph__tick" style="left:80%"/></div><span class="bar-graph__bar-label" style="left:24%">6% (671)</span></div></div><div class="bar-graph__bar-wrapper"><span class="bar-graph__bar-index">08:</span><span class="bar-graph__bar-key">S</span><div class="bar-graph__bar"><span class="bar-graph__bar-value" style="width:24%"/><div class="bar-graph__ticks"><span class="bar-graph__tick" style="left:20%"/><span class="bar-graph__tick" style="left:40%"/><span class="bar-graph__tick" style="left:60%"/><span class="bar-graph__tick" style="left:80%"/></div><span class="bar-graph__bar-label" style="left:24%">6% (669)</span></div></div><div class="bar-graph__bar-wrapper"><span class="bar-graph__bar-index">09:</span><span class="bar-graph__bar-key">N</span><div class="bar-graph__bar"><span class="bar-graph__bar-value" style="width:20%"/><div class="bar-graph__ticks"><span class="bar-graph__tick" style="left:20%"/><span class="bar-graph__tick" style="left:40%"/><span class="bar-graph__tick" style="left:60%"/><span class="bar-graph__tick" style="left:80%"/></div><span class="bar-graph__bar-label" style="left:20%">5% (575)</span></div></div><div class="bar-graph__bar-wrapper"><span class="bar-graph__bar-index">10:</span><span class="bar-graph__bar-key">C</span><div class="bar-graph__bar"><span class="bar-graph__bar-value" style="width:16%"/><div class="bar-graph__ticks"><span class="bar-graph__tick" style="left:20%"/><span class="bar-graph__tick" style="left:40%"/><span class="bar-graph__tick" style="left:60%"/><span class="bar-graph__tick" style="left:80%"/></div><span class="bar-graph__bar-label" style="left:16%">4% (477)</span></div></div><div class="bar-graph__bar-wrapper"><span class="bar-graph__bar-index">11:</span><span class="bar-graph__bar-key">U</span><div class="bar-graph__bar"><span class="bar-graph__bar-value" style="width:16%"/><div class="bar-graph__ticks"><span class="bar-graph__tick" style="left:20%"/><span class="bar-graph__tick" style="left:40%"/><span class="bar-graph__tick" style="left:60%"/><span class="bar-graph__tick" style="left:80%"/></div><span class="bar-graph__bar-label" style="left:16%">4% (467)</span></div></div><div class="bar-graph__bar-wrapper"><span class="bar-graph__bar-index">12:</span><span class="bar-graph__bar-key">Y</span><div class="bar-graph__bar"><span class="bar-graph__bar-value" style="width:16%"/><div class="bar-graph__ticks"><span class="bar-graph__tick" style="left:20%"/><span class="bar-graph__tick" style="left:40%"/><span class="bar-graph__tick" style="left:60%"/><span class="bar-graph__tick" style="left:80%"/></div><span class="bar-graph__bar-label" style="left:16%">4% (425)</span></div></div><div class="bar-graph__bar-wrapper"><span class="bar-graph__bar-index">13:</span><span class="bar-graph__bar-key">D</span><div class="bar-graph__bar"><span class="bar-graph__bar-value" style="width:12%"/><div class="bar-graph__ticks"><span class="bar-graph__tick" style="left:20%"/><span class="bar-graph__tick" style="left:40%"/><span class="bar-graph__tick" style="left:60%"/><span class="bar-graph__tick" style="left:80%"/></div><span class="bar-graph__bar-label" style="left:12%">3% (393)</span></div></div><div class="bar-graph__bar-wrapper"><span class="bar-graph__bar-index">14:</span><span class="bar-graph__bar-key">H</span><div class="bar-graph__bar"><span class="bar-graph__bar-value" style="width:12%"/><div class="bar-graph__ticks"><span class="bar-graph__tick" style="left:20%"/><span class="bar-graph__tick" style="left:40%"/><span class="bar-graph__tick" style="left:60%"/><span class="bar-graph__tick" style="left:80%"/></div><span class="bar-graph__bar-label" style="left:12%">3% (389)</span></div></div><div class="bar-graph__bar-wrapper"><span class="bar-graph__bar-index">15:</span><span class="bar-graph__bar-key">P</span><div class="bar-graph__bar"><span class="bar-graph__bar-value" style="width:12%"/><div class="bar-graph__ticks"><span class="bar-graph__tick" style="left:20%"/><span class="bar-graph__tick" style="left:40%"/><span class="bar-graph__tick" style="left:60%"/><span class="bar-graph__tick" style="left:80%"/></div><span class="bar-graph__bar-label" style="left:12%">3% (367)</span></div></div><div class="bar-graph__bar-wrapper"><span class="bar-graph__bar-index">16:</span><span class="bar-graph__bar-key">M</span><div class="bar-graph__bar"><span class="bar-graph__bar-value" style="width:12%"/><div class="bar-graph__ticks"><span class="bar-graph__tick" style="left:20%"/><span class="bar-graph__tick" style="left:40%"/><span class="bar-graph__tick" style="left:60%"/><span class="bar-graph__tick" style="left:80%"/></div><span class="bar-graph__bar-label" style="left:12%">3% (316)</span></div></div><div class="bar-graph__bar-wrapper"><span class="bar-graph__bar-index">17:</span><span class="bar-graph__bar-key">G</span><div class="bar-graph__bar"><span class="bar-graph__bar-value" style="width:12%"/><div class="bar-graph__ticks"><span class="bar-graph__tick" style="left:20%"/><span class="bar-graph__tick" style="left:40%"/><span class="bar-graph__tick" style="left:60%"/><span class="bar-graph__tick" style="left:80%"/></div><span class="bar-graph__bar-label" style="left:12%">3% (311)</span></div></div><div class="bar-graph__bar-wrapper"><span class="bar-graph__bar-index">18:</span><span class="bar-graph__bar-key">B</span><div class="bar-graph__bar"><span class="bar-graph__bar-value" style="width:8%"/><div class="bar-graph__ticks"><span class="bar-graph__tick" style="left:20%"/><span class="bar-graph__tick" style="left:40%"/><span class="bar-graph__tick" style="left:60%"/><span class="bar-graph__tick" style="left:80%"/></div><span class="bar-graph__bar-label" style="left:8%">2% (281)</span></div></div><div class="bar-graph__bar-wrapper"><span class="bar-graph__bar-index">19:</span><span class="bar-graph__bar-key">F</span><div class="bar-graph__bar"><span class="bar-graph__bar-value" style="width:8%"/><div class="bar-graph__ticks"><span class="bar-graph__tick" style="left:20%"/><span class="bar-graph__tick" style="left:40%"/><span class="bar-graph__tick" style="left:60%"/><span class="bar-graph__tick" style="left:80%"/></div><span class="bar-graph__bar-label" style="left:8%">2% (230)</span></div></div><div class="bar-graph__bar-wrapper"><span class="bar-graph__bar-index">20:</span><span class="bar-graph__bar-key">K</span><div class="bar-graph__bar"><span class="bar-graph__bar-value" style="width:8%"/><div class="bar-graph__ticks"><span class="bar-graph__tick" style="left:20%"/><span class="bar-graph__tick" style="left:40%"/><span class="bar-graph__tick" style="left:60%"/><span class="bar-graph__tick" style="left:80%"/></div><span class="bar-graph__bar-label" style="left:8%">2% (210)</span></div></div><div class="bar-graph__bar-wrapper"><span class="bar-graph__bar-index">21:</span><span class="bar-graph__bar-key">W</span><div class="bar-graph__bar"><span class="bar-graph__bar-value" style="width:8%"/><div class="bar-graph__ticks"><span class="bar-graph__tick" style="left:20%"/><span class="bar-graph__tick" style="left:40%"/><span class="bar-graph__tick" style="left:60%"/><span class="bar-graph__tick" style="left:80%"/></div><span class="bar-graph__bar-label" style="left:8%">2% (195)</span></div></div><div class="bar-graph__bar-wrapper"><span class="bar-graph__bar-index">22:</span><span class="bar-graph__bar-key">V</span><div class="bar-graph__bar"><span class="bar-graph__bar-value" style="width:4%"/><div class="bar-graph__ticks"><span class="bar-graph__tick" style="left:20%"/><span class="bar-graph__tick" style="left:40%"/><span class="bar-graph__tick" style="left:60%"/><span class="bar-graph__tick" style="left:80%"/></div><span class="bar-graph__bar-label" style="left:4%">1% (153)</span></div></div><div class="bar-graph__bar-wrapper"><span class="bar-graph__bar-index">23:</span><span class="bar-graph__bar-key">Z</span><div class="bar-graph__bar"><span class="bar-graph__bar-value" style="width:0%"/><div class="bar-graph__ticks"><span class="bar-graph__tick" style="left:20%"/><span class="bar-graph__tick" style="left:40%"/><span class="bar-graph__tick" style="left:60%"/><span class="bar-graph__tick" style="left:80%"/></div><span class="bar-graph__bar-label" style="left:0%">0% (40)</span></div></div><div class="bar-graph__bar-wrapper"><span class="bar-graph__bar-index">24:</span><span class="bar-graph__bar-key">X</span><div class="bar-graph__bar"><span class="bar-graph__bar-value" style="width:0%"/><div class="bar-graph__ticks"><span class="bar-graph__tick" style="left:20%"/><span class="bar-graph__tick" style="left:40%"/><span class="bar-graph__tick" style="left:60%"/><span class="bar-graph__tick" style="left:80%"/></div><span class="bar-graph__bar-label" style="left:0%">0% (37)</span></div></div><div class="bar-graph__bar-wrapper"><span class="bar-graph__bar-index">25:</span><span class="bar-graph__bar-key">Q</span><div class="bar-graph__bar"><span class="bar-graph__bar-value" style="width:0%"/><div class="bar-graph__ticks"><span class="bar-graph__tick" style="left:20%"/><span class="bar-graph__tick" style="left:40%"/><span class="bar-graph__tick" style="left:60%"/><span class="bar-graph__tick" style="left:80%"/></div><span class="bar-graph__bar-label" style="left:0%">0% (29)</span></div></div><div class="bar-graph__bar-wrapper"><span class="bar-graph__bar-index">26:</span><span class="bar-graph__bar-key">J</span><div class="bar-graph__bar"><span class="bar-graph__bar-value" style="width:0%"/><div class="bar-graph__ticks"><span class="bar-graph__tick" style="left:20%"/><span class="bar-graph__tick" style="left:40%"/><span class="bar-graph__tick" style="left:60%"/><span class="bar-graph__tick" style="left:80%"/></div><span class="bar-graph__bar-label" style="left:0%">0% (27)</span></div></div></div><figcaption>The most common letters in the "winning" Wordle word list</figcaption></figure></div></div></div><h2 id="finding-the-best-sequence-of-words">Finding the best sequence of words</h2><p>The next script I need will be my word finder, which I'll create in a file called <code>wordfinder.js</code>. To apply the letter-frequency information from the previous step, I'll need a script that can do two things:</p><ol><li>Find all the words from the list that contain a given set of letters (a.k.a. <em>find all words in the list that contain "a", "b", and "c"</em>)</li><li>Find all the words that do not include any of the letters in a given set (a.k.a. <em>find me all the five-letter words that do not contain: "abcdefghijk"</em>)</li></ol><pre><code class="hljs language-js"><span class="hljs-comment">// Find words that contain *all* input letters.</span>
<span class="hljs-keyword">const</span> <span class="hljs-title function_">inclusiveMatches</span> = (<span class="hljs-params">input, words</span>) => {
<span class="hljs-keyword">const</span> letters = [...input];
<span class="hljs-keyword">const</span> matches = words.<span class="hljs-title function_">filter</span>(<span class="hljs-function"><span class="hljs-params">word</span> =></span> {
<span class="hljs-keyword">const</span> hits = letters
.<span class="hljs-title function_">map</span>(<span class="hljs-function"><span class="hljs-params">letter</span> =></span> word.<span class="hljs-title function_">includes</span>(letter))
.<span class="hljs-title function_">filter</span>(<span class="hljs-function"><span class="hljs-params">hit</span> =></span> hit);
<span class="hljs-keyword">return</span> hits.<span class="hljs-property">length</span> == letters.<span class="hljs-property">length</span>;
});
<span class="hljs-keyword">return</span> matches;
};
<span class="hljs-comment">// Find words that do *not* contain any of the input letters.</span>
<span class="hljs-keyword">const</span> <span class="hljs-title function_">exclusiveMatches</span> = (<span class="hljs-params">input, words</span>) => {
<span class="hljs-keyword">const</span> alphabet = <span class="hljs-string">"abcdefghijklmnopqrstuvwxyz"</span>;
<span class="hljs-keyword">const</span> allowedLetters = [...input].<span class="hljs-title function_">reduce</span>(
<span class="hljs-function">(<span class="hljs-params">a, l</span>) =></span> a.<span class="hljs-title function_">replace</span>(l, <span class="hljs-string">""</span>),
alphabet
);
<span class="hljs-keyword">const</span> match = <span class="hljs-keyword">new</span> <span class="hljs-title class_">RegExp</span>(<span class="hljs-string">`^[<span class="hljs-subst">${allowedLetters}</span>]+$`</span>);
<span class="hljs-keyword">const</span> matches = words.<span class="hljs-title function_">filter</span>(<span class="hljs-function"><span class="hljs-params">word</span> =></span> match.<span class="hljs-title function_">test</span>(word));
<span class="hljs-keyword">return</span> matches;
};
</code></pre><p>I could just input the full dictionary from the last script, but there are a couple of other steps I can add to make my search more focused.</p><p>One of my objectives is to maximise the amount of the alphabet I cover with my opening Wordle guesses, so I know straight away that I'm not interested in words with repeated letters. A word where every letter is different is known as an "isogram", so with a little regEx magic we can filter out any isograms from our list.</p><pre><code class="hljs language-js"><span class="hljs-keyword">const</span> <span class="hljs-title function_">isIsogram</span> = string => !<span class="hljs-regexp">/(.).*\1/</span>.<span class="hljs-title function_">test</span>(string);
<span class="hljs-keyword">const</span> noRepeatedLetters = words.<span class="hljs-title function_">filter</span>(isIsogram);
</code></pre><p>Once we have our list of words that match our criteria (words that contain all the <code>MUST_HAVE_LETTERS</code> but none of the <code>MUST_NOT_HAVE_LETTERS</code>), I would find it really useful to sort them by the amount of vowels. Particularly when selecting our first or second Wordle guess, there could be <em>a lot</em> of options (we're dealing with a word-list with >12k entries, after all!). One of my heuristics is that I want my early guesses to include as few vowels as possible in order to maximise the word options on the later guesses.</p><p>Sorting by vowel-count is a two step process. I'll need a function to count the number of vowels in a given word, and I'll need a function to sort the words by that count.</p><pre><code class="hljs language-js"><span class="hljs-keyword">const</span> <span class="hljs-title function_">countVowels</span> = (<span class="hljs-params">word, vowels = <span class="hljs-string">"aeiou"</span></span>) => {
<span class="hljs-keyword">const</span> letters = [...word.<span class="hljs-title function_">toLowerCase</span>()];
<span class="hljs-keyword">const</span> justVowels = letters.<span class="hljs-title function_">filter</span>(<span class="hljs-function"><span class="hljs-params">char</span> =></span> vowels.<span class="hljs-title function_">indexOf</span>(char) > -<span class="hljs-number">1</span>);
<span class="hljs-keyword">return</span> justVowels.<span class="hljs-property">length</span>;
};
<span class="hljs-keyword">const</span> <span class="hljs-title function_">orderByVowels</span> = words =>
words
.<span class="hljs-title function_">map</span>(<span class="hljs-function"><span class="hljs-params">word</span> =></span> ({ word, <span class="hljs-attr">vowelCount</span>: <span class="hljs-title function_">countVowels</span>(word) }))
.<span class="hljs-title function_">sort</span>(<span class="hljs-function">(<span class="hljs-params">a, b</span>) =></span> (a.<span class="hljs-property">vowelCount</span> > b.<span class="hljs-property">vowelCount</span> ? -<span class="hljs-number">1</span> : <span class="hljs-number">1</span>))
.<span class="hljs-title function_">map</span>(<span class="hljs-function"><span class="hljs-params">object</span> =></span> object.<span class="hljs-property">word</span>)
.<span class="hljs-title function_">reverse</span>();
</code></pre><h2 id="putting-it-all-together">Putting it all together</h2><p>Assuming we've imported the word list and the aforementioned functions, the final flow of the script looks like this:</p><pre><code class="hljs language-js"><span class="hljs-keyword">const</span> onlyIsograms = words.<span class="hljs-title function_">filter</span>(isIsogram);
<span class="hljs-keyword">const</span> set = <span class="hljs-title function_">inclusiveMatches</span>(<span class="hljs-variable constant_">MUST_HAVE_LETTERS</span>, onlyIsograms);
<span class="hljs-keyword">const</span> subset = <span class="hljs-title function_">exclusiveMatches</span>(<span class="hljs-variable constant_">MUST_NOT_HAVE_LETTERS</span>, set);
<span class="hljs-keyword">const</span> sorted = <span class="hljs-title function_">orderByVowels</span>(subset);
sorted.<span class="hljs-title function_">map</span>(<span class="hljs-function"><span class="hljs-params">word</span> =></span> <span class="hljs-variable language_">console</span>.<span class="hljs-title function_">log</span>(word));
</code></pre><p>The only missing ingredients are the <code>MUST_HAVE_LETTERS</code> and <code>MUST_NOT_HAVE_LETTERS</code> variables.</p><p>The script could probably be automated to loop through all possible sequences of words and populate these values dynamically, but that's a bit more effort than I'm willing to expend. And besides, this is the stage that requires (in my mind, at least) some external judgement. I want to be able to pick Wordle guesses that <em>feel</em> right (otherwise I'll end up playing "yokul", "speug", or "phpht" - which <em>definitely</em> feel like cheat words even though they are genuine words from the Wordle allowed-list!).</p><p>I can get the best of both worlds by adding support for arguments into my script. If I can run <code>node wordfinder include=abc exclude=xyz</code>, for example, then I can quickly iterate over the options I like the look of manually. "Job done", in my opinion!</p><h2 id="adding-arguments-to-a-node-script">Adding arguments to a node script</h2><p>Adding support for arguments in my script involves setting up a <code>config</code> object with my default values, and then looping through the <code>process.argv</code> array (<code>process</code> is a variable that is built into Node and <code>argv</code> is a handy property which makes all the arguments available). For each argument, I'll split by <code>"="</code> to give me a key and value for each one. So running <code>node myscript foo=one bar=two</code> will give me key/values of <code>foo/one</code> and <code>bar/two</code>. If I include arguments that match the keys of my config options (in this case, <code>inc</code> and <code>exc</code>) then I can overwrite those values, and for the rest of my script <code>config.inc</code> will equal the value I passed into <code>inc=abc</code>.</p><pre><code class="hljs language-js"><span class="hljs-keyword">const</span> config = {
<span class="hljs-attr">inc</span>: <span class="hljs-string">""</span>,
<span class="hljs-attr">exc</span>: <span class="hljs-string">""</span>,
<span class="hljs-attr">max</span>: <span class="hljs-number">10</span>
};
process.<span class="hljs-property">argv</span>.<span class="hljs-title function_">map</span>(<span class="hljs-function"><span class="hljs-params">arg</span> =></span> {
<span class="hljs-keyword">const</span> argParts = arg.<span class="hljs-title function_">split</span>(<span class="hljs-string">"="</span>);
<span class="hljs-keyword">const</span> key = argParts[<span class="hljs-number">0</span>];
<span class="hljs-keyword">const</span> value = argParts[<span class="hljs-number">1</span>];
config[key] = value;
});
</code></pre><h2 id="do-it">Do it!</h2><p>Finally, after all this work, I can actually begin my search for a suitable sequence of opening guesses for Wordle!</p><p>The <code>node frequencies</code> script told me that the most frequently occurring consonants in the Wordle dictionary were <code>S R L T N</code>. To see if (by some miracle) there's a valid five letter word comprised of those exact five letters, I can run my <code>wordfinder</code> script.</p><pre><code class="hljs language-bash">node wordfinder inc=srltn
</code></pre><p>Unsurprisingly, there's no word that matches that criterion 😢. But ditching the <code>n</code> returns three words: "rotls", "slart", and "tirls". Hmmm, not exactly the "feels-right" words I was hoping for. But given that I've already ruled out vowels from my must-have letters on the basis that they're more useful later on, surely <code>s</code> is just as handy a letter to have up my sleeve late-game. Knocking that off the list opens up the options further.</p><p>Here are the opening options I experimented with:</p><table><thead><tr><th>Must-haves</th><th>Result count</th><th>Result words</th></tr></thead><tbody><tr><td>srltn</td><td>0</td><td/></tr><tr><td>srlt</td><td>3</td><td>rotls, slart, tirls</td></tr><tr><td>rltnd</td><td>0</td><td/></tr><tr><td>rltn</td><td>1</td><td>larnt</td></tr><tr><td>rltd</td><td>1</td><td>trild</td></tr><tr><td>rlnd</td><td>0</td><td/></tr><tr><td>rtnd</td><td>3</td><td>trend, drant, drent</td></tr></tbody></table><p>Of these, only <code>trend</code> really feels right to me, so my next step is to run the script again to find my options for the <em>second</em> guess. This time I'll include the <code>exc</code> argument to exclude the letters from my chosen first guess.</p><pre><code>node wordfinder inc=lycph exc=trend
</code></pre><p><img src="/images/articles/wordle-wordfinder-screenshot.png" alt="A screenshot of my terminal showing the output of my wordfinder script"/></p><table><thead><tr><th>Must-haves</th><th>Result count</th><th>Result words</th></tr></thead><tbody><tr><td>lycpm</td><td>0</td><td/></tr><tr><td>lycph</td><td>0</td><td/></tr><tr><td>lycp</td><td>0</td><td/></tr><tr><td>lyc</td><td>13</td><td>xylic, hylic, cymol, cylix, colby, cloys, clays, calyx, calmy, acyls, scaly, lucky, coaly</td></tr><tr><td>lypm</td><td>6</td><td>lymph, plumy, palmy, lumpy, imply, amply</td></tr></tbody></table><p>I can then repeat the process, picking new target letters for the <code>inc</code> argument and adding the previous word to the <code>exc</code> argument. This is, in itself, a fun little word game that I've now created for myself, and my little node scripts are what have made it possible.</p><p>I've uploaded the <a href="https://github.com/tomhazledine/wordle-analysis">complete version of these scripts to GitHub</a>, so you can download them and play with them yourself (and tweak them, if you like). What words did you end up choosing as your opening Wordle guesses? Be sure to tell me about them on Twitter!</p><h2 id="takeaways">Takeaways</h2><p>If you've made it this far, then hopefully you'll feel empowered to write some fun little scripts to help you out with day-to-day problems. Scripts like this one can save you loads of time (and can actually be used for useful things and not just silly word games). The idea of writing and running my own helper scripts used to feel daunting to me, but knowing they can be written in whichever language I fancy and that there are no stakes whatsoever makes it all seem more accessible.</p><p>So next time you're faced with a repetitive task that might take you hours to complete by hand, why not burn a few days writing a script to automate it instead...</p><div class="footnotes"><hr/><ol><li id="fn-1">If you’re looking for the most mathematically optimal programatic solution, I’d strongly recommend Three Blue One Brown’s <a href="https://www.youtube.com/watch?v=v68zYyaEmEA">excellent video about solving Wordle using information theory</a>.<a href="#fnref-1" class="footnote-backref">↩</a></li><li id="fn-2">I'm not very good at Wordle, but have never "failed" yet. Fewer guesses would be great but my main aim is <em>coverage</em>. I enjoy "clearing" the keyboard - that's the part of Wordle that I find most therapeutic. Getting three or more correct letters in the opening guess is actually quite stressful - the pressure's then on to get a good score!<a href="#fnref-2" class="footnote-backref">↩</a></li><li id="fn-3">Wordle is blissfully open source in the best tradition of the web. You can open a web inspector, and the all the code is right there for you to see. And for those of you wondering if anything changed when the NYT took over, the only difference to the word list was that they <em>removed</em> a couple of tricky words.<a href="#fnref-3" class="footnote-backref">↩</a></li><li id="fn-4">Here I've used ESM syntax and exported and imported using <code>export</code> and <code>import</code>, but that required setting up a <code>package.json</code> in my script's directory and declaring the script to be a module with <code>"type": "module"</code>. You can save this hassle by using the CJS <code>module.exports = words;</code> and <code>const { words } = require('./dictionary.js')</code> pattern, but I think that's ugly. <a href="https://dev.to/iggredible/what-the-heck-are-cjs-amd-umd-and-esm-ikm">Confused?</a><a href="#fnref-4" class="footnote-backref">↩</a></li></ol></div>Humility in software development2021-10-05T00:00:00Zhttps://tomhazledine.com/humility-in-tech/<p>I'm in the getting-to-know-you phase of a new job, and it's got me thinking about the aspects of my work-personality that I've <em>deliberately</em> chosen. I'm sure I've got plenty of inadvertent character traits that I've no idea of or control over (for good, I hope, but also inevitably for bad). But there are few I've consciously tried to foster, and the biggest of them is <strong>humility</strong><sup class="footnote-ref"><a href="#fn1" id="fnref1">[1]</a></sup>.</p>
<p>It's maybe not the perfect word to describe the concept I'm grasping at, and there's certainly no getting away from the <em>"ever so humble"</em> Uriah Heep references or the image of the 45<sup>th</sup> US president saying <em>"I am humble. I think I'm much more humble than you would understand"</em>. But it is the best word I've been able to come up with to encapsulate an idea that has been specifically valuable to me in the context of my career in tech.</p>
<h2 id="what-do-i-mean-by-%22humility%22" tabindex="-1">What do I mean by "humility"</h2>
<p>I guess the multi-word version of what I'm trying to express is a sentiment I've heard a few times: <em>never be the smartest person in the room.</em> That is great advice for anyone who wants to get better at what they do, and sets up a brilliant atmosphere for <em>learning</em>. And for it to work, there's a prerequisite for possessing the humility to admit to the limits of your knowledge and expertise. It can be tough to admit when you don't know something, but opening yourself up to that level of exposure is (in my experience) a sure-fire way to level-up your own skills.</p>
<p>I also extend this principle to non-skills-specific aspects of my work life. I've never suffered when working with someone who downplays their ability or is not confident in their own skills. I have, however, absolutely hated working with people who think they know it all. When I say "be humble" I mostly mean "don't be a person that people don't want to be around".</p>
<p>I aspire to be a >1✕ developer, and I want my work to be the best it can be. But the concept of a "10✕ developer" is toxic. I don't want to do the work of ten developers; I want to enable the other devs in my team to do better work. I want to be a "force multiplier" to the rest of my team; my colleagues should be able to do better work because I'm around.</p>
<h2 id="when-has-humility-helped-my-career%3F" tabindex="-1">When has humility helped my career?</h2>
<p>Everyone learns differently, but having someone directly explain a new concept or technique to me is my preferred way to learn. By exposing the gaps in my knowledge and actually asking for help, I get to move to the next level of understanding so much faster.</p>
<p>In meetings, I always try to ask the "dumb" questions. I'm sure someone else in the room will also be wondering what that TLI<sup class="footnote-ref"><a href="#fn2" id="fnref2">[2]</a></sup> means, or what the unspoken objectives are. No none has ever shouted at me for being an idiot, and in fact quite the opposite has happened. People <em>thank</em> me for engaging and asking questions (both in public and in private).</p>
<p>When reviewing PRs I try to never, ever, ever, be dismissive of the submission another developer has spent time on. Even if my gut instinct is to literally throw my laptop out of the window in the hope I never have to view that disgusting code again, I step back and take a breath and remind myself how often my gut has been wrong in the past. Plainly put, my gut has terrible instincts. A simple friendly interrogation can often expose the intention that I've missed, or the bug I didn't realise needed fixing. No-one writes bad code on purpose, and even if I've got a lot to say and many changes to suggest, I always try to present my ideas as possibilities rather than edicts. A good PR should be a conversation, and the ultimate responsibility lies with the author.</p>
<p>When digging through "legacy" code, it always pays to have humility. It's <em>so</em> easy to think my ideas are so much better than the garbage code that I've been forced to work with. The temptation to refactor everything into <em>my</em> style and to flow the way <em>I</em> think it should flow is real. Every now and then I'm right - my version <em>is</em> better. But that is so rarely the case. If I start with the understanding that the people who came before me knew what they were doing, I'm more able to spot the subtle bugs they've avoided or the elegant performance improvements they've made by not doing things the way I would expect.</p>
<p>I wish I'd learnt that last point so much sooner - it would have saved a lot of time and painful on-call refactoring!</p>
<h2 id="when-has-humility-hurt-my-career%3F" tabindex="-1">When has humility hurt my career?</h2>
<p>Another mantra I live with is that "a plan is not a strategy unless the <em>opposite</em> approach is also valid". "Eat food" is not a strategy, because not eating is not a valid option. "Don't eat meat" <em>is</em> a strategy, because eating meat is perfectly normal. Each option has pros and cons, but both are valid: it's a strategic choice. So what is the opposite approach to humility? "Arrogance", I guess. And yes, sadly, in tech choosing to be arrogant can be a beneficial strategy. Act like an expert, and some people will treat you like an expert.</p>
<p>Honestly, a lot of this tech-career malarkey is a perception game. If you make a good first impression and your superiors decide that you are "a smart person with good ideas" then you can go a long way without having to actually prove yourself to anyone. If, like me, your whole schtick is "your ideas are better than mine; tell me more about them", then some people might take that to mean that you don't have any ideas. And a bad impression sticks much more thoroughly than a good one. A CEO's good opinion, once lost, is lost forever.</p>
<p>I've had to fight this a couple of times. But even though I'm asking the "stupid" questions and deferring to others' expertise, <strong>I know exactly how good I am</strong>. I also know how valuable this tactic has been to my long-term career advancement (and general mental wellbeing!), so I've made it my policy never to compromise. A healthy team instinctively knows the value of humility already. And an unhealthy team; well, that's not a team I want to be a part of.</p>
<h2 id="the-elephant-in-the-room-caveat" tabindex="-1">The elephant-in-the-room caveat</h2>
<p>I'm a white straight male with a private school education<sup class="footnote-ref"><a href="#fn3" id="fnref3">[3]</a></sup>. That alone gives me more leeway to risk appearing "lesser" than the people I'm working with. Despite having done nothing to earn it, people are more likely to give me the benefit of the doubt and assume I'm "playing devil's advocate" or just "digging into the nitty gritty details" rather than seeing me as "slow" or having "not grasped the overall concept".</p>
<p>People with different backgrounds may well find the suggestion to "show more humility" to be unhelpful and downright insulting. So I think my advice only applies to people who never find their abilities questioned. If you've had to fight tooth and nail just to get in the room, then you probably need (and deserve) to show confidence. It's those of us who get all that respect for free that need to tone it down a bit. If more developers <em>in my position</em> showed a bit more humility, it would open up a lot of doors for other people.</p>
<h2 id="is-it-a-useful-concept-for-guiding-my-actions%3F" tabindex="-1">Is it a useful concept for guiding my actions?</h2>
<p>I've gone from self-taught and self-employed to junior-dev to mid- to senior- to whatever-you-want-to-call-what-I-am-now. I've not always been able to live up to my ideals of humility, and have learnt painfully from those experiences. But when I've been able to live up to (or at least near) my lofty ambitions - those are the times I've done my best work. Those are the times I've made the best friends, had the best experiences, and made the best advancements in my career.</p>
<p>My new colleagues know very little about me beyond my CV, so if all I can do in these early stages is show that I'm open to conversations and happy to learn wherever possible, then I'll call that a win.</p>
<hr class="footnotes-sep"/>
<section class="footnotes">
<ol class="footnotes-list">
<li id="fn1" class="footnote-item"><p>I know, I know! But trust me; keep reading. The idea is less gross than it sounds. I promise. <a href="#fnref1" class="footnote-backref">↩︎</a></p>
</li>
<li id="fn2" class="footnote-item"><p>A TLI is a Three Letter Initialism. Har har. <a href="#fnref2" class="footnote-backref">↩︎</a></p>
</li>
<li id="fn3" class="footnote-item"><p>"Lord, grant me the confidence of an average white man." - I know this is a thing, and it sucks. <a href="#fnref3" class="footnote-backref">↩︎</a></p>
</li>
</ol>
</section>Podcasting: what gear do you need?2021-09-27T00:00:00Zhttps://tomhazledine.com/podcasting-equipment/<p>We're in the setup-and-planning phase of launching a new podcast at my new job, and once again I found myself sharing my <em>Podcast Equipment 101</em> tips with a team who are interested in podcasting but have no prior experience of working with audio. This is not the first time I've written a guide like this, so I'm publishing this one here for posterity (mostly so I don't have to type it all out again <em>next</em> time).</p>
<p>I've broken this guide into three sections: an overview of the <a href="#essential-equipment">essential equipment</a> that you'll need to record a podcast, my personal recommendation for <a href="#so-what-should-i-use%3F">what I think you should use</a> <em>(if you're looking for a <strong>TL;DR</strong>, this is it)</em>, and a few <a href="#good-technique-is-more-important-than-good-equipment">quick hacks for better recordings</a></p>
<h2 id="essential-equipment" tabindex="-1">Essential equipment</h2>
<p>To record a good podcast, there are three things that you will unequivocally[^1] need. Yes, this is just my opinion. Yes, you can make "a" podcast with less than this. No, I don't think that podcast would be a "good" one. The quality bar is low, but there is a bar. Get these three things sorted and your quality is guaranteed to be "good enough".</p>
<ol>
<li><a href="#1%2E-microphones">A "good" microphone</a></li>
<li><a href="#2%2E-connections">A connection between your mic and your computer</a></li>
<li><a href="#3%2E-software">Software to record the audio from your mic</a></li>
</ol>
<h3 id="1.-microphones" tabindex="-1">1. Microphones</h3>
<p>It is possible to record a podcast with <em>anything</em>; your phone's earbuds, a dictaphone, your laptop's built in speaker, anything you can think of. The content is of course more important than the equipment you use. <strong>However</strong>, the biggest distinction between amateur and professional podcasts is the audio quality, and the single best investment you can make is to get a decent microphone. The difference between even a "cheap" dedicated microphone and the basic methods outlined above is <em>massive</em>.</p>
<p>There are three types of microphones that are good for podcasting:</p>
<ul>
<li><strong>Dynamic mics</strong> Good examples of these are the basic <a href="https://www.amazon.co.uk/SHURE-SM58-LC-SM58-Vocal-Microphone/dp/B000CZ0R42">Shure SM58</a> (~£85) and the fancy <a href="https://www.amazon.co.uk/Shure-SM7B-Microphone/dp/B0002E4Z8M">SM7B</a> (~£350). Dynamic mics have a small diaphragms (the membrane that converts sound waves to electrical signals), and only pick up noise that is very close to them. Dynamic mics are good for stage performers, but also useful for people recording in noisy environments (although you'll have to remember to keep your mouth very close to the mic).</li>
<li><strong>Condenser mics</strong> Good examples of these are the <a href="https://www.amazon.co.uk/Rode-Microphones-R%C3%98DE-NT1-Vocal-gold/dp/B0002PSCQM">Rode NT1-A</a> (~£150) and the <a href="https://www.andertons.co.uk/neumann-u87-ai-microphone-set-nickel-finish-w-ea87-shockmount-u87set">Neumann U87</a> (~£2,500) These are often "studio grade" microphones, and are great for recording musical instruments and crisp, clear voices. Most entry-level podcasting mics are condensers with large diaphragms, which means they pick up every sound in the room (be careful that you're not recording in a noisy environment). These will require a shock mount to eliminate noise from vibrations in your desk and a pop shield to remove the "bangs" that plosive sounds ("a","b", "p", etc.) make when you speak directly into a sensitive microphone.</li>
<li><strong>Lavalier mics</strong> Good examples of these are the <a href="https://www.thomann.de/gb/rode_lavalier_go.htm">Rode Lavelier Go</a> (~£50) and the <a href="https://www.thomann.de/gb/sennheiser_mke404.htm">Sennheiser MKE40-4</a> (~£300). These are the tiny clip-on mics that people often use at conferences, and as a result are often part of a "wireless" setup. These are not common for "normal" podcasting, but can be useful for setups with multiple non-technical people in the same room. Mic technique is less of an issue with these, but the sound quality suffers a little compared to standard desk-mounted mics.</li>
</ul>
<h3 id="2.-connections" tabindex="-1">2. Connections</h3>
<p>Once you've got your mic, you need a way to connect it to your computer so you can record it. For most microphones, this will involve running an XLR cable from the mic into an "audio interface" that connects to your computer via USB. There are many, many types of audio interfaces that range in price and features, but you can't go wrong with the <a href="https://www.pmtonline.co.uk/focusrite-scarlett-solo-3rd-gen-usb-audio-interface">Focusrite Scarlett Solo</a> (~£100) which allows you to connect a single mic to your computer, or the <a href="https://www.gear4music.com/Recording-and-Computers/Focusrite-Scarlett-2i2-3rd-Gen/2YSF">Scarlett 2i2</a> (~£150) that allows you to connect two mics at the same time.</p>
<p>Some podcast-focused microphones don't require an audio interface, and can be connected to the computer directly via USB. Good examples of these are the <a href="https://www.amazon.co.uk/Rode-Microphones-NT-USB-Microphone/dp/B00KQPGRRE">Rode NT-USB</a> (~£140) and the <a href="https://www.amazon.co.uk/Audio-Technica-AT2020USB-PLUS-USB-Microphone/dp/B00B5ZX9FM">Audio-Technica AT2020USB+</a> (~$130).</p>
<p>You'll also need a stand for your microphone. Boom arms give you the best flexibility for an at-the-desk setup (and also give you that drive-time radio DJ vibe), but small tripods that sit directly in front of you are fine. Remember, though, that for best results your mouth needs to be as close to the mic as possible, and this can be awkward if you're trying to use a keyboard with a desk-mount between you and the keys. Boom arms offer the best UX.</p>
<p>Depending on what mic you choose, you may need some extra peripherals. The Shure SM7B, for example, needs a pre-amp to boost the signal before it goes into the audio interface. These are uncommon, however, and most common setups don't require anything beyond the items outlines in this doc.</p>
<h3 id="3.-software" tabindex="-1">3. Software</h3>
<p>Audio software is an overwhelming mess of options, so I'll do my best to be brief here. What software you use depends on your role in the podcast. If you're going to be doing any editing, you'll need a full Digital Audio Workstation. If you're just a host or guest, then all you need to worry about is capturing the audio from your microphone and there are plenty of simple options for this.</p>
<p>A Digital Audio Workstation (often referred to as a DAW, which can be said <em>"dee, a, double-u"</em> or <em>"door"</em> depending on how nerdy you're feeling) is a full-featured audio editing suite. With a DAW you can do everything from trimming the ends of an audio file to pitch-shifting your guest's voice so it sounds like they're singing. Good examples are the Mac-native <a href="https://www.apple.com/uk/logic-pro/">Logic Pro</a> (~£180), the cross-platform industry stalwart <a href="https://www.avid.com/pro-tools">Pro Tools</a> (~$30 per month), and the open-source <a href="https://www.audacityteam.org/">Audacity</a> (free). These are often expensive and can be intimidating to new users, but a good DAW will give you everything you need to record, edit, and export your podcast episodes.</p>
<p>If you're releasing a podcast, <em>someone</em> on your team will need to have a DAW and know how to use it, but it's definitely overkill to expect guests and non-editing hosts to buy one and learn it. While you can "just record" with a DAW (or even a halfway-house like Garage Band), any simple audio-capture software is fine. If you're using a Mac, then Quicktime is perfect. I'm sure there are Windows/Linux equivalents to Quicktime, but they've never crossed my radar. The key features are being able to select which input device you're recording from (a.k.a. the fancy mic you've just plugged in) and to make sure that you're recording in high quality (without any of the compression that tools like Zoom and Skype introduce to save bandwidth).</p>
<h2 id="so-what-should-i-use%3F" tabindex="-1">So what should I use?</h2>
<p>If you're not the person doing the editing, then get a decent USB microphone (either of the two mentioned above would be fine). Get a cheap boom arm and record with Quicktime.</p>
<p>If you want the "best" podcasting setup, then get an SM7B instead of the USB mic. You'll then also need a <a href="https://www.andertons.co.uk/cloud-microphones-cloudlifter-cl-1-single-channel-powered-preamp-for-ribbon-dynamic-microphones-cloudliftercl1">Cloudlifter</a> preamp, an XLR cable, and the Scarlett Solo audio interface. Okay, so maybe The Best<sup>TM</sup> is subjective, but this setup is at least the Industry Standard<sup>TM</sup>.</p>
<p>And if you're the designated editor, then I'd recommend buying (and learning) Logic Pro. Logic is the DAW I use (after having tried a lot of the alternative options over the years) and you can see a list of some of the other bits of podcasting equipment that I use in the <a href="https://tomhazledine.com/uses/#audio-gear">Audio Gear section of my <code>/uses</code> page</a></p>
<h2 id="good-technique-is-more-important-than-good-equipment" tabindex="-1">Good technique is more important than good equipment</h2>
<p>The most important caveat to all of this is that <em>if you don't use the equipment properly there's no point using it at all</em>. Even if you spend thousands on mics and peripherals, if you're pointing the mic in the wrong direction and recording in an echoey room then your audio will still sound terrible.</p>
<p>Quick hacks for better recordings:</p>
<ul>
<li>Always always <strong>always use headphones</strong>. You'll probably be talking to someone over Skype or Zoom, and you only want your microphone to pick up the noises that <em>you</em> make.</li>
<li>Keep your mouth as close to the mic as possible, and make sure you're talking into the correct part (most dynamic mics record through their top, whereas condensers often record from the side).</li>
<li>Don't record somewhere noisy, and especially if you're using a condenser mic be sure to turn off any humming computers or air-conditioners etc.</li>
<li>Minimize the amount of flat, reflective surfaces near where you are recording. Sound reflections from large flat walls can cause feedback loops or just bad-sounding echoes. The worst place to record is in a perfectly square room with bare walls. Pull the curtains, and try to record in a room with lots of soft furnishings.</li>
<li>The surface directly <em>behind</em> the mic (from your perspective) can have a big impact on the sound: if your mic is on your desk in front of you, put a jumper or cushion between the mic and your computer to help minimize unwanted reflections.</li>
</ul>
<p>There's a lot more to podcasting than I've covered here (hosting, editing, marketing, actually making good content) but this guide feels appropriately focused on the equipment side of things. Perhaps I'll come back to this well for more how-to articles in future. If that's something you'd be interested in, <a href="https://twitter.com/thomashazledine">let me know on Twitter (I'm @thomashazledine)</a>. Thanks for reading, and good luck with your podcasting adventure. Be sure to share your show with me when you've launched it, I can't wait to hear it.</p>Line graphs with React and D3.js2021-07-21T00:00:00Zhttps://tomhazledine.com/line-graphs-with-react-svg-d3/<p>A while ago I wrote <a href="https://tomhazledine.com/web-audio-delay/">an article about the Web Audio API</a> with an interactive demo. The primary visualisation for that demo was a series of graphs that displayed the live frequency data of the sounds made by the demo. Since then, several people have asked about how the graphs were made and the answer is way too confusing when expressed at tweet-length, so this post is a full explanation.</p><p>The core principle is to use React to generate an SVG that updates ("reacts"?) when the data changes, and to use certain features of D3.js to make these calculations easier.</p><figure><div id="delay-block"/><figcaption>Push the "pulse" button to make some bleepy-bloopy sounds. The frequency data will be shown in the graph in real-time.</figcaption></figure><nav class="toc"><h2>In this article</h2><ol><li class="stack--small"><a href="#what-are-the-tools-well-be-using">What are the tools we'll be using? <span class="icon icon--link"/></a></li><li class="stack--small"><a href="#the-basics-converting-an-array-of-data-into-an-svg-path">The basics: converting an array of data into an SVG path <span class="icon icon--link"/></a><ol class="stack--small"><li class="stack--small"><a href="#step-1-parsing-the-data">Step 1. parsing the data <span class="icon icon--link"/></a></li><li class="stack--small"><a href="#step-2-creating-a-react-component-with-an-svg-in-it">Step 2. creating a React component with an SVG in it <span class="icon icon--link"/></a></li><li class="stack--small"><a href="#step-3-mapping-our-data-to-the-visual-domain-using-d3js">Step 3. mapping our data to the visual domain using D3.js <span class="icon icon--link"/></a></li></ol></li><li class="stack--small"><a href="#make-it-dynamic">Make it dynamic <span class="icon icon--link"/></a></li><li class="stack--small"><a href="#why-is-this-graph-so-different-from-the-original-demo">Why is this graph so different from the original demo? <span class="icon icon--link"/></a></li><li class="stack--small"><a href="#core-ideas-to-use-in-your-own-work">Core ideas to use in your own work <span class="icon icon--link"/></a></li></ol></nav><h2 id="what-are-the-tools-well-be-using">What are the tools we'll be using?</h2><ol><li><strong>SVG</strong><sup id="fnref-1"><a href="#fn-1" class="footnote-ref">1</a></sup>: using an SVG <code>path</code> feels (to me) like a really intuitive way to draw our graph lines. You can inspect the results in any web-inspector just like any other DOM node, and we can style the SVG with CSS in the same way we would style any other component.</li><li><strong>React</strong>: the React "virtual DOM" makes this kind of graph really easy. When the data changes, React efficiently handles the re-rendering of the graph. Because we're using a stream of real-time audio data that updates several times a second, we don't need to worry about "tweening" or animating the graph line.</li><li><strong>D3.js</strong>: D3 is a powerful tool that <em>could</em> handle the creation of the whole graph. I find React more intuitive for this kind of component work, but D3 has some amazing data-wrangling tools that we can leverage to make our life easier. Specifically, the mapping of data into a layout-friendly domain and the creation of the actual <code>path</code> values that will form the main part of the graph.</li></ol><p>The aim is to optimize for an easy workflow that leans into the tools and techniques that I already use when creating websites. I already use React for anything that needs to be "dynamic". SVG uses markup that looks familiar to anyone who knows HTML, and can be styled with the same (S)CSS workflow that I use for every web project. D3 is the only graph-specific tool I'm using, and that's because it has some very specific utilities that make the rest of the process much easier.</p><h2 id="the-basics-converting-an-array-of-data-into-an-svg-path">The basics: converting an array of data into an SVG path</h2><p>So what's happening in the example at the top of the page? At the most basic level, the Web Audio API code is giving us an array of numbers for each moment in time. These numbers are then converted to a format that can be used as line data in an SVG. That SVG is then rendered on the page. Here's a simpler example, with all the "decorative" elements (axes, labels, etc.) removed, and using a simpler (and static) data set:</p><figure><div id="simple-line-graph"/><figcaption>A simple static line graph drawn using SVG.</figcaption></figure><h3 id="step-1-parsing-the-data">Step 1. parsing the data</h3><p>To build this reduced version of the graph, we'll start off with some representative data. We'll use fewer numbers than the real audio data, but the basic format is the same. The Web Audio API "analyser" gives us an array of frequency data for each moment in time<sup id="fnref-2"><a href="#fn-2" class="footnote-ref">2</a></sup>, so what we're using for this demo is functionally the same:</p><pre><code class="hljs language-js"><span class="hljs-keyword">const</span> exampleData = [<span class="hljs-number">34</span>, <span class="hljs-number">44</span>, <span class="hljs-number">32</span>, <span class="hljs-number">78</span>, <span class="hljs-number">184</span>, <span class="hljs-number">221</span>, <span class="hljs-number">171</span>, <span class="hljs-number">26</span>, <span class="hljs-number">62</span>, <span class="hljs-number">5</span>];
</code></pre><p>The obvious thing that we're missing from this data is a second axis. Each "node" in the graph line represents an <code>x</code> and <code>y</code> coordinate, and our data only contains a single dimension.</p><p>With the real audio data, the array of numbers represents the volume of the signal at a series (an array, even?) of specifc frequencies. This level (ranging between <code>0</code> and <code>255</code>) becomes what we plot on the y-axis. Because each value represents an evenly-spaced slice of the frequency data we can distribute our points evenly across our graph. We can use the index-value from the array to create the x-axis data we need.</p><p>To convert our raw array of numbers into a format that can be used to draw a graph, we want each item to be an object with an <code>x</code> and <code>y</code> value. A quick <code>map()</code> over our array creates this for us:</p><pre><code class="hljs language-js"><span class="hljs-keyword">const</span> exampleData = [<span class="hljs-number">34</span>, <span class="hljs-number">44</span>, <span class="hljs-number">32</span>, <span class="hljs-number">78</span>, <span class="hljs-number">184</span>, <span class="hljs-number">221</span>, <span class="hljs-number">171</span>, <span class="hljs-number">26</span>, <span class="hljs-number">62</span>, <span class="hljs-number">5</span>];
<span class="hljs-keyword">const</span> cleanData = exampleData.<span class="hljs-title function_">map</span>(<span class="hljs-function">(<span class="hljs-params">item, i</span>) =></span> ({ <span class="hljs-attr">x</span>: i, <span class="hljs-attr">y</span>: item }));
<span class="hljs-comment">// cleanData = [</span>
<span class="hljs-comment">// {x: 0, y: 34},</span>
<span class="hljs-comment">// {x: 1, y: 44},</span>
<span class="hljs-comment">// {x: 2, y: 32},</span>
<span class="hljs-comment">// {x: 3, y: 78},</span>
<span class="hljs-comment">// etc...</span>
<span class="hljs-comment">// ];</span>
</code></pre><h3 id="step-2-creating-a-react-component-with-an-svg-in-it">Step 2. creating a React component with an SVG in it</h3><p>Now we're armed with some useable data, we can create a React component to draw this data onto the page.</p><p>What we want is to draw an rectangular <code><svg></code> element with a single <code><path></code> within it. An SVG <code><path></code> gets it's shape from a data prop called <code>d</code>. We'll create a <code>line</code> state value that will populate this prop, and hard code this value for now (and add <em>our</em> data in the next step).</p><p>Note that we're doing something triksy with the SVG's <code>viewBox</code> property here. Because in future steps we'll be mapping our data to coordinates within the SVG, we need a <em>fixed</em> set of dimensions for our image. But we also want our graph to be responsive and fit whatever screen it's being shown on (even in narrow contexts!) so we're setting <strong>absolute</strong> values for our <code>viewBox</code> and a <strong>relative</strong> value (a percentage) for our <code>width</code>. Combined with the <code>preserveAspectRatio="none"</code> prop, this means that we only need to do our coordinate calculations once, and CSS will work it's magic to ensure the graph responds to different widths correctly.</p><pre><code class="hljs language-js"><span class="hljs-keyword">import</span> <span class="hljs-title class_">React</span>, { useState } <span class="hljs-keyword">from</span> <span class="hljs-string">"react"</span>;
<span class="hljs-keyword">const</span> <span class="hljs-title function_">ExampleLineGraph</span> = (<span class="hljs-params">{ data }</span>) => {
<span class="hljs-keyword">const</span> [line, setLine] = <span class="hljs-title function_">useState</span>(<span class="hljs-string">"M0,200 L100,100 L400,100 L500,0"</span>);
<span class="hljs-keyword">return</span> (
<span class="xml"><span class="hljs-tag"><<span class="hljs-name">svg</span>
<span class="hljs-attr">className</span>=<span class="hljs-string">"graph--example"</span>
<span class="hljs-attr">width</span>=<span class="hljs-string">"100%"</span>
<span class="hljs-attr">height</span>=<span class="hljs-string">"200"</span>
<span class="hljs-attr">viewBox</span>=<span class="hljs-string">"0 0 500 200"</span>
<span class="hljs-attr">preserveAspectRatio</span>=<span class="hljs-string">"none"</span>
></span>
<span class="hljs-tag"><<span class="hljs-name">path</span> <span class="hljs-attr">className</span>=<span class="hljs-string">"graph__data"</span> <span class="hljs-attr">d</span>=<span class="hljs-string">{line}</span> /></span>
<span class="hljs-tag"></<span class="hljs-name">svg</span>></span></span>
);
};
<span class="hljs-keyword">export</span> <span class="hljs-keyword">default</span> <span class="hljs-title class_">ExampleLineGraph</span>;
</code></pre><p>We'll also want to set some CSS rules so our SVG looks the way we want it to:</p><pre><code class="hljs language-css">// Declare our colour custom properties (<span class="hljs-selector-tag">a</span><span class="hljs-selector-class">.k</span><span class="hljs-selector-class">.a</span>. CSS variables)
<span class="hljs-selector-pseudo">:root</span> {
<span class="hljs-attr">--grey</span>: <span class="hljs-number">#dad8d2</span>;
<span class="hljs-attr">--primary</span>: <span class="hljs-number">#00b7c6</span>;
}
// Set the <span class="hljs-attribute">border</span> for the whole graph
<span class="hljs-selector-class">.graph--example</span> {
<span class="hljs-attribute">border</span>: <span class="hljs-number">1px</span> solid <span class="hljs-built_in">var</span>(--grey);
}
// Set the colour of the line (and remove any defualt "fill" our line may have)
<span class="hljs-selector-class">.graph__data</span> {
fill: none;
stroke: <span class="hljs-built_in">var</span>(--primary);
}
</code></pre><p>Then all that's left is to mount our example graph on the page. Don't forget that even though we're passing in our <code>cleanData</code> value here, we're not actually using it yet (that will come in step #3):</p><pre><code class="hljs language-js"><span class="hljs-keyword">const</span> exampleData = [<span class="hljs-number">34</span>, <span class="hljs-number">44</span>, <span class="hljs-number">32</span>, <span class="hljs-number">78</span>, <span class="hljs-number">184</span>, <span class="hljs-number">221</span>, <span class="hljs-number">171</span>, <span class="hljs-number">26</span>, <span class="hljs-number">62</span>, <span class="hljs-number">5</span>];
<span class="hljs-keyword">const</span> cleanData = exampleData.<span class="hljs-title function_">map</span>(<span class="hljs-function">(<span class="hljs-params">item, i</span>) =></span> ({ <span class="hljs-attr">x</span>: i, <span class="hljs-attr">y</span>: item }));
<span class="hljs-title class_">ReactDOM</span>.<span class="hljs-title function_">render</span>(
<span class="xml"><span class="hljs-tag"><<span class="hljs-name">ExampleLineGraph</span> <span class="hljs-attr">data</span>=<span class="hljs-string">{cleanData}</span> /></span></span>,
<span class="hljs-variable language_">document</span>.<span class="hljs-title function_">getElementById</span>(<span class="hljs-string">"simple-line-graph"</span>)
);
</code></pre><figure><div id="super-simple-line-graph"/><figcaption>A hard-coded SVG <code><path></code>.</figcaption></figure><h3 id="step-3-mapping-our-data-to-the-visual-domain-using-d3js">Step 3. mapping our data to the visual domain using D3.js</h3><p>With our basic line being successfully drawn within the SVG, the next step is to hook in our "real" data. Currently we're passing in an array of data via the <code>data</code> prop, but we're not using it yet. We need to convert the array of x/y objects into a format that can be understood by the <code>d</code> property of an SVG <code><path></code> element.</p><p>This is where D3.js comes in handy. The D3 graphing library is often used to generate entire graphs, but all we need in this instance are a couple of helper functions to make our data-conversion a little easier:</p><pre><code class="hljs language-js"><span class="hljs-keyword">import</span> { line, scaleLinear } <span class="hljs-keyword">from</span> <span class="hljs-string">"d3"</span>;
</code></pre><ul><li>D3's <code>line()</code> function will handle the formatting of our data: turning it into a valid SVG <code>d</code> value.</li><li>D3's <code>scaleLinear()</code> function will help us convert our raw data into spatially-aware values (a.k.a. mapping our <strong>frequency</strong> values into <strong>pixel</strong> values)</li></ul><p>The key concept at work here is that of <strong>ranges</strong> and <strong>domains</strong>. Our raw data exists in a frequency "domain", and the values are frequency values (i.e. <code>34 Hz</code>, <code>44 Hz</code>, <code>32 Hz</code> etc. ). To draw this data with our graph, we'll need to convert those frequencies into <em>pixel</em> values; we need to transform them from the frequency domain to the spatial "range". This is what we'll use <code>scaleLinear</code> for.</p><p>Defining the spatial range is relatively straightforward. In our first pass at the graph, we hardcoded the <code>viewBox</code> values for our graph. To make life easier for ourselves (and to avoid accidentally changing an important value in one place but missing it in another) we'll define our width and height values in a <code>layout</code> object that we can reference every time we need to use a layout value. We'll then update the <code>height</code> and <code>viewBox</code> props to use these values:</p><pre><code class="hljs language-js"><span class="hljs-keyword">const</span> layout = {
<span class="hljs-attr">width</span>: <span class="hljs-number">500</span>,
<span class="hljs-attr">height</span>: <span class="hljs-number">200</span>
};
</code></pre><pre><code class="hljs language-js">height={layout.<span class="hljs-property">height</span>}
viewBox={<span class="hljs-string">`0 0 <span class="hljs-subst">${layout.width}</span> <span class="hljs-subst">${layout.height}</span>`</span>}
</code></pre><p>The next piece of the puzzle is to setup our D3 functions as values we can use in the rest of our code. As well as our line generator (using the D3 <code>line()</code> function), we'll need to <em>scales</em><sup id="fnref-3"><a href="#fn-3" class="footnote-ref">3</a></sup>: one for <code>x</code> and one for <code>y</code>. For each scale we'll set a range from <code>0</code> to the corresponding layout value (width for <code>x</code> and height for <code>y</code>):</p><pre><code class="hljs language-js"><span class="hljs-keyword">const</span> graphDetails = {
<span class="hljs-attr">xScale</span>: <span class="hljs-title function_">scaleLinear</span>().<span class="hljs-title function_">range</span>([<span class="hljs-number">0</span>, layout.<span class="hljs-property">width</span>]),
<span class="hljs-attr">yScale</span>: <span class="hljs-title function_">scaleLinear</span>().<span class="hljs-title function_">range</span>([layout.<span class="hljs-property">height</span>, <span class="hljs-number">0</span>]),
<span class="hljs-attr">lineGenerator</span>: <span class="hljs-title function_">line</span>()
};
</code></pre><p>There are two more things we need to setup for our line generator before we can use it. Firstly we need to define the domain of our data, and secondly we need to tell the generator which values to use for which axis.</p><p>Setting the data domain looks similar to how we set the range. We're defining the minimum and maximum values our data might be. The <code>x</code> domain is the simpler of the two, being as it's the number of items in our array (more complicated datasets would need more complicated domains, of course). As for the <code>y</code> domain, we know that our audio analyser provides a maximum frequency value of <code>255 Hz</code>, so we can hardcode this value (I like to add a bit of "headroom" to the graph for purely visual reasons, so I've bumped the value from <code>255</code> to <code>280</code>).</p><pre><code class="hljs language-js">graphDetails.<span class="hljs-property">xScale</span>.<span class="hljs-title function_">domain</span>([<span class="hljs-number">0</span>, data.<span class="hljs-property">length</span> - <span class="hljs-number">1</span>]);
graphDetails.<span class="hljs-property">yScale</span>.<span class="hljs-title function_">domain</span>([<span class="hljs-number">0</span>, <span class="hljs-number">280</span>]);
</code></pre><p>Assigning the correct data values to the line generator is one of the more esoteric parts of D3 (it confused me for a long time!). Things made a lot more sense when I realised that we're defining a function that will be run for each item in our dataset <em>when the line generator is actually used</em>.</p><p>Our data (provided to this component via the <code>data</code> prop) is in object format (e.g. <code>{x: 3, y: 78}</code>). We've handily named our values <code>"x"</code> and <code>"y"</code> but they could be called <em>anything</em>, so we need to tell our line generator which values to use. This is also the point where the values are run through our scale functions. So for each item (<code>d</code>) in our dataset, we're passing our <code>d["x"]</code> value into the <code>xScale</code> function, and returning that computed value. (and ditto for <code>y</code>).</p><pre><code class="hljs language-js">graphDetails.<span class="hljs-property">lineGenerator</span>.<span class="hljs-title function_">x</span>(<span class="hljs-function"><span class="hljs-params">d</span> =></span> graphDetails.<span class="hljs-title function_">xScale</span>(d[<span class="hljs-string">"x"</span>]));
graphDetails.<span class="hljs-property">lineGenerator</span>.<span class="hljs-title function_">y</span>(<span class="hljs-function"><span class="hljs-params">d</span> =></span> graphDetails.<span class="hljs-title function_">yScale</span>(d[<span class="hljs-string">"y"</span>]));
</code></pre><p>And now that the line generator is all set up, we can finally use it when we intialise our <code>lineData</code> state, and then we can plumb that into the <code><path></code> element within our SVG:</p><pre><code class="hljs language-js"><span class="hljs-keyword">const</span> [lineData, setLineData] = <span class="hljs-title function_">useState</span>(<span class="hljs-function">() =></span>
graphDetails.<span class="hljs-title function_">lineGenerator</span>(data)
);
</code></pre><pre><code class="hljs language-js"><path className=<span class="hljs-string">"graph__data"</span> d={lineData} />
</code></pre><p>At this point, the full component looks like this:</p><pre><code class="hljs language-js"><span class="hljs-keyword">import</span> <span class="hljs-title class_">React</span>, { useState } <span class="hljs-keyword">from</span> <span class="hljs-string">"react"</span>;
<span class="hljs-keyword">import</span> { line, scaleLinear } <span class="hljs-keyword">from</span> <span class="hljs-string">"d3"</span>;
<span class="hljs-keyword">const</span> <span class="hljs-title function_">ExampleLineGraph</span> = (<span class="hljs-params">{ data }</span>) => {
<span class="hljs-keyword">const</span> layout = {
<span class="hljs-attr">width</span>: <span class="hljs-number">500</span>,
<span class="hljs-attr">height</span>: <span class="hljs-number">200</span>
};
<span class="hljs-keyword">const</span> graphDetails = {
<span class="hljs-attr">xScale</span>: <span class="hljs-title function_">scaleLinear</span>().<span class="hljs-title function_">range</span>([<span class="hljs-number">0</span>, layout.<span class="hljs-property">width</span>]),
<span class="hljs-attr">yScale</span>: <span class="hljs-title function_">scaleLinear</span>().<span class="hljs-title function_">range</span>([layout.<span class="hljs-property">height</span>, <span class="hljs-number">0</span>]),
<span class="hljs-attr">lineGenerator</span>: <span class="hljs-title function_">line</span>()
};
graphDetails.<span class="hljs-property">xScale</span>.<span class="hljs-title function_">domain</span>([<span class="hljs-number">0</span>, data.<span class="hljs-property">length</span> - <span class="hljs-number">1</span>]);
graphDetails.<span class="hljs-property">yScale</span>.<span class="hljs-title function_">domain</span>([<span class="hljs-number">0</span>, <span class="hljs-number">280</span>]);
graphDetails.<span class="hljs-property">lineGenerator</span>.<span class="hljs-title function_">x</span>(<span class="hljs-function"><span class="hljs-params">d</span> =></span> graphDetails.<span class="hljs-title function_">xScale</span>(d[<span class="hljs-string">"x"</span>]));
graphDetails.<span class="hljs-property">lineGenerator</span>.<span class="hljs-title function_">y</span>(<span class="hljs-function"><span class="hljs-params">d</span> =></span> graphDetails.<span class="hljs-title function_">yScale</span>(d[<span class="hljs-string">"y"</span>]));
<span class="hljs-keyword">const</span> [lineData, setLineData] = <span class="hljs-title function_">useState</span>(<span class="hljs-function">() =></span>
graphDetails.<span class="hljs-title function_">lineGenerator</span>(data)
);
<span class="hljs-keyword">return</span> (
<span class="xml"><span class="hljs-tag"><<span class="hljs-name">svg</span>
<span class="hljs-attr">className</span>=<span class="hljs-string">"graph--example"</span>
<span class="hljs-attr">width</span>=<span class="hljs-string">{</span>"<span class="hljs-attr">100</span>%"}
<span class="hljs-attr">height</span>=<span class="hljs-string">{layout.height}</span>
<span class="hljs-attr">viewBox</span>=<span class="hljs-string">{</span>`<span class="hljs-attr">0</span> <span class="hljs-attr">0</span> ${<span class="hljs-attr">layout.width</span>} ${<span class="hljs-attr">layout.height</span>}`}
<span class="hljs-attr">preserveAspectRatio</span>=<span class="hljs-string">"none"</span>
></span>
<span class="hljs-tag"><<span class="hljs-name">path</span> <span class="hljs-attr">className</span>=<span class="hljs-string">"graph__data"</span> <span class="hljs-attr">d</span>=<span class="hljs-string">{lineData}</span> /></span>
<span class="hljs-tag"></<span class="hljs-name">svg</span>></span></span>
);
};
<span class="hljs-keyword">export</span> <span class="hljs-keyword">default</span> <span class="hljs-title class_">ExampleLineGraph</span>;
</code></pre><figure><div id="final-simple-line-graph"/><figcaption>The line renders the real data 🎉</figcaption></figure><h2 id="make-it-dynamic">Make it dynamic</h2><p>The beauty of using React for a project like this is that there aren't many more steps required to make the graph dynamically respond to changing data. Because we've already initialised our data with a <code>useState</code> hook, we can make React watch for changes in the data with a standard <code>useEffect</code> hook.</p><pre><code class="hljs language-js"><span class="hljs-title function_">useEffect</span>(<span class="hljs-function">() =></span> {
<span class="hljs-keyword">if</span> (data) {
<span class="hljs-comment">// Calculate the data line</span>
<span class="hljs-keyword">const</span> newLine = graphDetails.<span class="hljs-title function_">lineGenerator</span>(data);
<span class="hljs-title function_">setLineData</span>(newLine);
}
}, [data]);
</code></pre><p>With this hook in place, whenever the <code>data</code> prop changes the path <code>d</code> will be recalculated and re-rendered. Note again that we're not digging into the specifics of <em>how</em> the data value is changed. That would require an entire article on Web Audio API analyser nodes. This example just covers the graphing component, not the actual data generation.</p><figure><div id="delay-block-simplified"/><figcaption>Push the "pulse" button to make some bleepy-bloopy sounds.</figcaption></figure><h2 id="why-is-this-graph-so-different-from-the-original-demo">Why is this graph so different from the original demo?</h2><p>There are a couple of obvious differences between this simplified demo and full-featured frequency graph at the top of this page.</p><ol><li>The first graph uses a logarithmic scale for the x-axis (note how on the simple example, the peaks occur squished toward the left of the graph). Instead of using D3's <code>scaleLinear()</code> function, the original graph uses <code>scaleLog()</code> combined with actual frequency values (rather than the evenly spaced indexes that we've used for the simpler demo). If you're implementing this for yourself, you can apply the same concepts that we used on our y-axis: i.e. mapping between a visual range and a data domain.</li><li>The first graph has tick marks (the helpful guiding lines that make it clear where each frequency goes). These are implemented with <code><line></code> nodes in SVG, drawing simple lines from one x/y coordinate to another using the <code>x1</code>, <code>y1</code>, <code>x2</code>, and <code>y2</code> props.</li><li>The first graph has labels for the axes. Truth be told, I've dodged some complexity here by only marking the minimum and maximum frequency values. This allows me to add the labels with traditional markup and CSS. If you wanted to mark incremental values along either axis, you would need to use the <code>yScale</code> or <code>xScale</code> functions to find the visual positions of the exact values you wanted to mark.</li></ol><h2 id="core-ideas-to-use-in-your-own-work">Core ideas to use in your own work</h2><p>As I just mentioned, in this article we've made a much-simplified version of the original frequency graph. This is deliberate, because every graph will inevitably have it's own quirks and idiosyncrasies that make it different from all other graphs (and thus hard to explain in a basic manner). I wish it wasn't so, but you can only abstract so far before your all-singing-all-dancing reusable graphing component has so many options that it's actually <em>harder</em> to use that building every graph individually from scratch. This article is an attempt to show a few core concepts that can be reused in lots of different contexts. Those core concepts are:</p><ol><li>Use whatever tools that you are most comfortable with (but sometimes add in a <em>little</em> bit of something new to make your life easier). I'm comfortable using React and SVG, so I'm mostly just using those. Graph-maths can get hard, though, so I'm using <em>as little D3 as possible</em> to make my life easier.</li><li>SVG and React are great tools for creating visualisations. If visualising data is something that you want to do on a regular basis, these tools are great ones to learn. They're versatile enough to handle all sorts of data-vis and the skills you learn will also be useful in the wider web ecosystem (so you're not wasting too much time learning an esoteric system that's only useful for one thing).</li><li>Speaking of esoteric systems, D3.js is really powerful but can be daunting too. It can do so much, but also has it's own quirky ways of doing a lot of things. My response is to just use the parts that I actually need, which in this case is just the line generator and linear scale calculator. Then I'm free to let the more conventional tools (a.k.a. React and hand-coded vanilla SVG) to do the bulk of the work.</li></ol><div class="footnotes"><hr/><ol><li id="fn-1">If SVGs are new to you (or you just need a quick refresher on their syntax) I've written a <a href="https://tomhazledine.com/svg-markup/">primer on SVG markup</a> that might be useful.<a href="#fnref-1" class="footnote-backref">↩</a></li><li id="fn-2"><em><strong>A quick note about getting audio data:</strong> we're skipping the details of getting raw frequency data from the Web Audio API. The important concept to Google is the <code>createAnalyser</code> method. <code>createAnalyser()</code> is available on any audio "context" (a concept we touched on in detail in the <a href="https://tomhazledine.com/web-audio-delay/">original Web Audio API post</a>).</em><a href="#fnref-2" class="footnote-backref">↩</a></li><li id="fn-3">Note that for the yScale we're setting the range in reverse (from <code>height</code> to <code>0</code>). This is so that our line starts at the <em>bottom</em> of the graph.<a href="#fnref-3" class="footnote-backref">↩</a></li></ol></div>Learning (and doing) in public2021-04-30T00:00:00Zhttps://tomhazledine.com/learning-and-doing-in-public/<p>I've always struggled with <em>finishing</em> things, but lately I've found that forcing myself to release projects early has had some amazing effects. Flushed with that success, I'm going one step further and making myself publicly accountable for things I'm <em>planning</em> to do.</p>
<h2 id="what's-been-working%3F" tabindex="-1">What's been working?</h2>
<p>As I mentioned in my last post, <a href="https://tomhazledine.com/falling-back-in-love-with-music/">I've been making music again</a>. A big part of the enthusiasm I've maintained for music lately comes from the fast feedback loop that comes from learning in public.</p>
<p>With my past musical endeavours I would ticker for ages in private, building up to a "grand reveal" that often simply never happened. This time around I've made the conscious decision to share early; I'm posting experiments and practices to my instagram account, and releasing pieces onto YouTube as soon as their "done". This has taken two big leaps for me:</p>
<ol>
<li>I've never shared "practice" before. I'm objectively bad at the instrument I'm currently learning (my modular synth), and my practices could be seen by experts as embarrassing. I'm teaching myself not to care about this.</li>
<li>I've had to change my definition of "done". The pieces I'm releasing are not finely crafted opuses. I'm releasing as soon as I have ~4 minutes of sound with a beginning, middle, and end.</li>
</ol>
<p>The results so far have been awesome. It's great to get positive feedback; hearing that people like my work drives me to make more of it. Negative feedback would be useful, too, but I've (mercifully?) not had much of that yet. Although on social media a <em>lack</em> of any feedback is a strong signal. The good stuff gets a reaction, and the bad stuff sinks into obscurity. Either way, I'm learning what works and what doesn't work, and that guides my future experiments and practice sessions.</p>
<p>And in the spirit of <em>"doubling down on what seems to be working"</em>, I'm now expanding my <em>"do all the things in public!"</em> policy to some other areas.</p>
<h2 id="what-am-i-committing-myself-to%3F" tabindex="-1">What am I committing myself to?</h2>
<p>Firstly, some simple rolling objectives. I've had these goals in place since January as part of my <a href="https://tomhazledine.com/year-of-writing/">Year of Writing</a>:</p>
<ul>
<li>A written piece on this blog, once a month.</li>
<li>Twelve videos about my modular synth on YouTube by the end of the year.</li>
<li>Fifty-two modular-themed posts on Instagram by the end of the year.</li>
</ul>
<p>They sound modest, but just look back at <a href="https://tomhazledine.com/archive/">my blogging history</a> to see how <em>consistent</em> (or should I say <em>inconsistent</em>) my track record is. I can be very productive in short bursts, but keeping up momentum for a full calendar year is not something I often manage.</p>
<hr/>
<p>Secondly, I'm setting myself some stretch goals. These are things I <em>want</em> to get done Soon™️, and would love to get shipped before the end of the summer. But if I get to December and haven't hit any of these goals but <em>have</em> met my rolling objectives, I'll still call the year a "Win".</p>
<ol>
<li>
<p><strong>Sort out the newsletter.</strong> Should this site (should <em>I</em>) have a newsletter? I started a podcast-focused one last year, and it <s>flopped</s> was less of a success than I'd hoped for. I'm currently on the fence about whether I should have a general newsletter for all the content published on this site, or whether it should focus solely on my Modular Synth activities. What I <em>do</em> know, is that this site should have a proper "call to action" for people who enjoy the content.</p>
</li>
<li>
<p><strong>Release <a href="https://www.npmjs.com/package/picobel">Picobel.js</a> as a React component.</strong> I (still!) really enjoy working in React, and Picobel feels like the perfect use-case for the <code><Component /></code> pattern. It also gives me more control over state changes (like "playing" or "paused" or "loading") which lead to a nicer (and more reliable) experience for the end user.</p>
</li>
<li>
<p><strong>Make an interactive JavaScript version <em>In C</em>.</strong> This one's is just for me, and just for fun. I'd love to recreate Terry Riley's seminal minimalist musical composition, <em>In C</em>, on my modular. Alas, both my skills and sequencing equipment aren't up to the task just yet. As a stopgap, I'm going to recreate it in the browser using the Web Audio API.</p>
</li>
</ol>
<h2 id="what-are-the-consequences-of-failure%3F" tabindex="-1">What are the consequences of failure?</h2>
<p>In all honesty, the consequences of missing my targets are mercifully slight. Given the amount of readers this blog gets (very few) and the number of people clamouring for my new projects (none), there are no tangible consequences for missing my "deadlines".</p>
<p>But failure stings, even if the bounds of success are arbitrary and imaginary. Fear of failing to meet my rolling goals has already pushed me to publish more frequently here and on IG and YouTube. Ask me in six months how this experiment has gone. I've reasonably confident that I'll feel good about it by then.</p>Falling back in love with music2021-03-31T00:00:00Zhttps://tomhazledine.com/falling-back-in-love-with-music/<p>Over the last year I've fallen in love with making music again, and it's all thanks to <strong>modular synthesisers</strong>. But to understand why this is such a big step for me, you need to know just how surprising it is that I'm now <em>"in to"</em> electronic music.</p>
<figure><img src="/images/articles/modular_with_succulent.jpg" alt=""/><figcaption>My growing eurorack modular synthesiser (complete with obligatory succulent)</figcaption></figure>
<h3 id="falling-out-of-love-with-music" tabindex="-1">Falling out of love with music</h3>
<p>I've always been "into" music, but I used to be <em>really</em> into music. I played in lots of bands, tried my hand as a session musician, tried to start a record label, ran a music blog (for, like, years!). But in 2013 it all sort of fizzled out.</p>
<p>In retrospect, I ran out of steam. On paper things were going in the right direction - I could legitimately call myself a session musician, I sold a couple of commercial compositions, and I even had a song of mine on the radio (that £6 royalty cheque was a Big Deal, let me tell you!), but I never really saw enough success in any area to persevere. Except, maybe, the blog - <em>that</em> worked and was fun.</p>
<p>Making that blog was so fun, in fact, that 12 years later I now have a pretty decent career as a frontend engineer making things for the web. But the "music" aspect soon fell by the wayside. I shuttered the blog in 2013 I never looked back, focusing instead on frontend engineering.</p>
<hr class="hr--bump"/>
<h3 id="the-wilderness-years" tabindex="-1">The wilderness years</h3>
<p>For a long time I was more interested in writing code than writing music. A couple of my projects touched on music but the focus was always on the technology, never on actually creating music. The Web Audio API featured heavily in my <a href="https://codepen.io/tomhazledine/full/VKvNJg">CodePen tinkering</a> and I built a whole conference talk around my <a href="https://tomhazledine.com/web-audio-delay/">experiments recreating a guitarists' delay pedal in JavaScript</a>. But at no point was I <em>making music</em>. Not even in private or just for fun. If I remembered, I'd run a few drills on the guitar to maintain my muscle memory but I wasn't ever composing or creating anything new.</p>
<h3 id="guitar-pedals%3A-the-ultimate-gateway-drug" tabindex="-1">Guitar pedals: the ultimate gateway drug</h3>
<p>This story hinges on a single moment: I saw something and was instantly hooked. But the groundwork for that one incident was laid over a long, long time. How did I get into electronica? It's a big leap for an ex-musician who always focused on <em>acoustic</em> instruments (heck, I wrote my undergraduate dissertation on the evolution of English folk music, and my post-grad thesis was an extremely contrived deep-dive into the "album" as a medium).</p>
<p>But there were two things that lit the spark of interest that would ignite into a full-blown obsession with modular synths: GAS and the Web Audio API.</p>
<blockquote>
<p>GAS - a.k.a. Gear Acquisition Syndrome. As in, <em>"oh boy, those new Strymon guitar pedals have triggered my GAS. RIP my bank account"</em>.</p>
</blockquote>
<p>Guitar pedals are a fantastic introduction into the world of modular synths: small units that do one thing to an audio signal. It already sounds very "modular", am I right? And I've <em>always</em> loved guitar pedals. In fact, my GAS covers all guitar-related gear. Microphones, audio interfaces, cables, pedals, the guitars themselves. I'm not going to lie; I get just as much (if not more!) pleasure from owning all these nice shiny things than I do from actually using them.</p>
<p>The second factor that paved the way for an interest in modular was the Web Audio API. A side effect of playing around with using JavaScript to create and manipulate audio was that (almost by a process of osmosis) I was exposed to concepts and terms that originated in the world of hardware synthesisers. VCAs and VCOs became part of my lexicon, along with more familiar concepts like filtering and mixing. Bit by bit, without even being aware of it at the time, I was learning how to synthesise sound.</p>
<h3 id="enter-stage-right%3A-modular-synthesisers" tabindex="-1">Enter stage right: modular synthesisers</h3>
<p>So what was that single moment that opened my eyes to the world of modular synths? Quite simply, it was a YouTube video. I've kept a weather eye on Andrew Huang's musical adventures for a while, but I stumbled on his <a href="https://youtu.be/cWslSTTkiFU">Modular Synthesis Explained</a> video while looking for something cool to include in my newsletter. Specifically, the shot of his mega system at work at <a href="https://youtu.be/cWslSTTkiFU?t=930">15:30 mins</a> touched something deep inside me. I didn't know what it was, or what it was doing, or how it worked, but I knew I wanted one for myself.</p>
<p>Andrew has made <a href="https://www.youtube.com/watch?v=oFadopWxKjw">lots</a> of <a href="https://www.youtube.com/watch?v=UXEyEIo-WtA">videos</a> about his eurorack system over the years (it's fascinating to see the system grow over time) and he's made some fantastic "explainer" videos. YouTube, it turns out, is a great place to ramp up your GAS to extreme levels. there are so many great creators making so many great videos, both showing off their great modular music but also making fantastic tutorial content.</p>
<hr class="hr--bump"/>
<h3 id="why-has-modular-made-me-fall-back-in-love-with-making-music%3F" tabindex="-1">Why has modular made me fall back in love with making music?</h3>
<p>I'm about a year into my modular journey at this point. It was twelve-ish months ago that I first learned what a modular synth was, and another few months of research and obsessive YouTube bingeing before I bit the bullet and bought my first modules. Now my system is getting to the point where I can really make some fun pieces with it, and I'm excited about what the future holds.</p>
<p>I've embraced the idea of "learning in public", so I'm sharing all my experiments and clumsy, early attempts at music making on <a href="https://www.instagram.com/tomhazledine/">my Instagram feed</a>. This is totally unlike anything I ever did with any other instrument, and goes against my instinct of getting things as close to perfect as possible before sharing them with anyone. And it feels great! There's no pressure to be "good", because everyone knows I'm making this stuff up as I go along. And the feedback I've already had is just amazing - there's so much more validation in being "okay" at something in public than there is in being amazing at it in private. I should have adopted this approach a long time ago.</p>
<p>And when (on the rare occasion) I make something I'm <em>really</em> proud of, I'll take the time to edit it and <a href="https://youtu.be/ZVc8TzQ_z4U">put it on YouTube</a>. YouTube feels more "permanent" than the 'gram, and already I'm seeing that the life of a YT video continues long (so far!) after the initial launch. Whereas on Instagram, once a post is off the front page it is gone forever (as far as engagement goes).</p>
<p>As for <em>why</em> I'm enjoying it so much, there are several reasons:</p>
<ol>
<li><strong>I love the sounds I can make with this machine.</strong> I've been listening to <em>a lot</em> of ambient music made on modular systems, and I'm relishing the challenge of recreating that style all by myself (and I still have a lot of learning and practicing to do!).</li>
<li><strong>I love that making music on a modular doesn't require a computer at all.</strong> Manipulating sounds with real tactile controls and making connections with physical wires is exponentially more fun than using a software alternative. Sure, you might be able to create the same sounds "in the box" (maybe), but you can't create the same experience. Modular synths are <em>playable</em>. You can build muscle memory, you can manipulate two parameters <em>at the same time</em> (try doing that with a mouse). You can "ride" a fader or pot with so much more precision than you can in a software GUI.</li>
<li><strong>I love that I get to build the instrument that I want to play.</strong> There are so many modules out there that there's a high chance that even my small system is unique. I get to choose the exact tools required to make the kinds of sounds I want to make.</li>
<li><strong>I love that I can make make sounds that are impossible (or at least highly impractical) to make on a computer.</strong> There are some things that you can do on a modular synthesiser that you just cannot do in software.</li>
<li><strong>I love how the instrument looks.</strong> I'm not going to lie, the aesthetic of modular is probably a good third of my motivation for getting into it. The arrays of potentiometers, the vast selection of I/O, the blinking LEDs (oh, the blinking LEDs!), and the general space-ship-control-panel vibe. I love it all.</li>
</ol>
<p>There are plenty more reasons, too. And looking at that list written down, I'm now thinking that each one of those points could be an article all by itself (so stay tuned for more modular nerdery here soon). But suffice it to say, I've fallen hard for modular synthesisers.</p>
<p>If you're interested in my modular journey, be sure to checkout <a href="https://youtu.be/ZVc8TzQ_z4U">my YouTube channel</a> where I post "completed works" and <a href="https://www.instagram.com/tomhazledine/">my Instagram feed</a> where I post my experiments and test things out.</p>
<iframe width="100%" height="450" src="https://www.youtube.com/embed/ZVc8TzQ_z4U" title="YouTube video player" frameborder="0" allow="accelerometer; autoplay; clipboard-write; encrypted-media; gyroscope; picture-in-picture" allowfullscreen="allowfullscreen"/>The year of writing2021-02-07T00:00:00Zhttps://tomhazledine.com/year-of-writing/<p>For 2019 I experimented with an alternative to a traditional new year's resolution: a "Yearly Theme". It was such a success that I repeated the process in 2020, and now I'm laying the foundations for my "theme" for 2021.</p>
<h2 id="themes-are-not-resolutions" tabindex="-1">Themes are <em>not</em> resolutions</h2>
<p>To be blunt, I've come to view new year's resolutions as a load of nonsense. They're often small and frivolous goals set with little thought and even less expectation of success. But I am a big believer in occasionally taking-stock and assessing my progress. Progress in what, you may ask? Well, pretty much everything.</p>
<p>And because everything takes a pause at the end of the year (by design, as Christmas is a big deal in my house), the January-December cadence works well as a natural period to look back on and plan ahead for. Although perfectly arbitrary, the incrementing of a number on the calendar is a useful boundary.</p>
<h2 id="what-are-yearly-themes%3F" tabindex="-1">What are Yearly Themes?</h2>
<p>Inspired by <a href="https://www.thethemesystem.com/">CGP Grey and Myke Hurley from the Cortex podcast</a>, a yearly theme is a guiding idea or principle that you apply to your life for the whole year. The "theme" concept is deliberately vague enough to mean different things to different people. So what does a yearly theme mean to me?</p>
<ul>
<li><strong>It should be specific enough to provide guidance when faced with a decision.</strong> You should be able to envisage a situation where your theme would be useful when faced with a "should I do <em>x</em> or <em>y</em>" decision.</li>
<li><strong>It should, by nature, be strategic, and therefore have a logical opposite.</strong> The theme is all about choosing a specific path for yourself, so it would have no value if it was something that you would naturally do anyway. A theme like "the year of getting older one day at a time" is meaningless, because it's something you're inevitably going to do: there is no viable alternative. In a similar vein, I don't think that "the year of being successful" is a good theme because no one sets out to <em>not</em> be successful, and in many ways success is a measure, not a specific goal. You can't plan to be successful; success is an outcome of your plans.</li>
<li><strong>It must be memorable, because you should have it at the front of your mind throughout the year.</strong> It's also helpful if other people in you life can remember it, too. That helps with accountability and also gives those around you an insight into why you choose to do the things you do.</li>
</ul>
<h2 id="past-themes%3A" tabindex="-1">Past themes:</h2>
<p>This is not my first rodeo, and I've had a theme for each of the last couple of years.</p>
<ul>
<li><strong>2019: The year of shipping.</strong> My plan here was to increase the amount of projects that I finished. By default I'm a tinkerer, and rarely see things through to completion, so setting this as an explicit goal meant that I shipped more things. In many ways it was a great success<sup class="footnote-ref"><a href="#fn1" id="fnref1">[1]</a></sup> and heralded a change in mental attitude, but there's still more work to be done. I <em>do</em> ship more now that I used to, but I still err on the side of Infinite Tinkering.</li>
<li><strong>2020: The year of action.</strong> I wasn't happy with the name for this one, but <a href="https://twitter.com/ed_the_coder">@ed_the_coder</a> convinced me to roll with "action". In my mind it was more akin to the (less catchy) "year of personal responsibility". In many, many ways this ended up being a great theme for 2020.</li>
</ul>
<h2 id="2021%3A-the-year-of-%3F%3F%3F" tabindex="-1">2021: The year of ???</h2>
<p>For 2021 I'm hedging my bets and choosing a dual-sided theme. Last year I got into modular synths in a big way, and as a result I've fallen back in love with music<sup class="footnote-ref"><a href="#fn2" id="fnref2">[2]</a></sup>. So I'm determined to write and release some of my own music this year. I'm also doubling down on blogging as a concept that I think is important. Some of my favourite websites are blogs, and as someone who writes code for a living, have an outlet for my more <em>creative</em> technological experiments is valuable both for my career and as a general learning tool.</p>
<p>So this year I'll be <em>writing</em> music. And what is blogging if not <em>writing</em>? 2021: the year of music? The year of blogging? Why not both? <strong>2021 is going to be my Year of Writing</strong>. Ask me in 2022 how I got on...</p>
<hr class="footnotes-sep"/>
<section class="footnotes">
<ol class="footnotes-list">
<li id="fn1" class="footnote-item"><p>In 2019 I upped my workplace productivity, and consistently released weekly episodes of the <em>A Question of Code</em> podcast (where we have <a href="https://aquestionofcode.com/43-yearly-themes-2020/">talked at length about Yearly Themes</a> before). <a href="#fnref1" class="footnote-backref">↩︎</a></p>
</li>
<li id="fn2" class="footnote-item"><p>How I fell <strong>out</strong> of love with music is an interesting tale, but one for another time. <a href="#fnref2" class="footnote-backref">↩︎</a></p>
</li>
</ol>
</section>RSS in 2021 (yes, it's still a thing)2021-01-17T00:00:00Zhttps://tomhazledine.com/adding-rss/<p>I'm trying out a "recipe blogger" style of writing<sup class="footnote-ref"><a href="#fn1" id="fnref1">[1]</a></sup>. That is to say, I'm going to share a simple set of technical instructions but first I'll tell you a rambling story about how this recipe reminds me of how butterflies sip dew from the golden whatever high atop the thing... Unlike food bloggers, however, I'm self-aware enough to include handy section links so you can choose your own adventure.</p>
<ul>
<li>Rambling story: <a href="#why-im-an-rss-fanboy">Why I'm an RSS fanboy</a></li>
<li>Actually useful content: <a href="#how-i-added-an-rss-feed-to-my-eleventy-blog">How I added an RSS feed to my Eleventy blog</a></li>
<li>My finished feed: <a href="https://tomhazledine.com/feed.xml">tomhazledine.com/feed.xml</a> (go ahead, follow me!)</li>
</ul>
<h2 id="why-i'm-an-rss-fanboy" tabindex="-1">Why I'm an RSS fanboy</h2>
<p>Strangely, RSS is one of my favourite parts of the web. I say "strangely" because I'm primarily a front end developer who loves having control over how a website <em>looks</em>, and it's hard to imagine a medium that gives you <em>less</em> control over the display of your content than RSS does. It's purely a content-based medium. As it says in the name; it's <em>really simple</em>.</p>
<p><em>So why do I love it so?</em></p>
<ol>
<li><strong>It's the underpinning tech that makes podcasts possible.</strong> You might not have heard, but I'm <a href="">really</a> <a href="">in to</a> <a href="">podcasting</a> and think it's one of the single greatest innovations of my lifetime. And part of the power of the medium is that the distribution is 100% decentralised. Literally anyone (with access to the bare minimum tech requirements) can distribute their show on the same footing as giant media companies. Here at the start of 2021 it looks like that might be in danger of changing, but the core concept of "what I think a podcast is" still feels solid and robust and defensible. People will still be making the kind of podcasts I like decades from now, thanks in part to RSS.</li>
<li><strong>It's a direct delivery to me from writers I want to read.</strong> So much of modern content delivery is predicated on virallity and share-ability, but sometimes there are writers (dare we even say <em>bloggers</em>) who I know in advance I want to read (or at least be made aware of) everything they publish. RSS makes that process (you guessed it) <em>Really Simple</em>. There are no algorithms to filter what I'm shown, and no adverts and holding-pages injected into the flow to drive me crazy. It's just the content. And while for lazy readers like me there <em>are</em> timelines to wade through, provided your reader-app is a good one this process is straightforward and manageable.</li>
</ol>
<h2 id="how-i-added-an-rss-feed-to-my-eleventy-blog" tabindex="-1">How I added an RSS feed to my Eleventy blog</h2>
<p>So given that I love RSS so much, why don't I have an RSS feed for my own blog? Well, you'll be pleased to hear that now I do. Part of the fun of maintaining your own blog is that you tend not to get too much for free, especially if (like me) you've chosen to roll your own site with minimal help from pre-made themes or templates. Adding features like RSS feeds (or search, or favicons, or meta tags) is all in the journey. Thankfully I'm not mad enough to have rolled my own static site generator (yet), so with a little help from the folks at Eleventy I was able to add an RSS feed with ~very little fuss at all~ <em>some fuss after all</em>.</p>
<h3 id="1.-create-a-feed.xml-page" tabindex="-1">1. Create a <code>feed.xml</code> page</h3>
<p>Add a <code>feed.md</code> file in your content directory, and use the <code>permalink</code> frontmatter to tell Eleventy that you want the page to be created as an XML file, and that you want the page to be <a href="https://www.11ty.dev/docs/collections/#option-exclude-content-from-collections">excluded from collections</a> (note that I write my frontmatter in yaml).</p>
<pre><code class="language-yaml">permalink: feed.xml
excludeFromCollections: true
</code></pre>
<h3 id="2.-create-the-template-structure-for-your-feed" tabindex="-1">2. Create the template structure for your feed</h3>
<p>The aim here is to build the XML document that will serve as your RSS feed. This is very similar to generating a regular archive or category page, except we want the output to be XML rather than HTML. The differences are subtle, but there are a few things to watch out for if we want our feed to be valid. Dates, for instance, need to be formatted in a very specific way.</p>
<p>The Eleventy ecosystem is on hand to help us out with here with the <a href="https://www.11ty.dev/docs/plugins/rss/">RSS Plugin</a>. It doesn't do everything for us, but does give us access to some handy RSS/XML-related functions: <code>getNewestCollectionItemDate</code> and <code>dateToRfc3339</code> being particularly useful when it comes to correctly formatting date-info for our feed.</p>
<p>The docs for the Eleventy RSS Plugin also provide a sample template file, but I'd advise treading with caution here. While helpful, converting the sample template to match my exact project was trickier than I'd first anticipated. Be sure to go through the sample template line-by-line and make sure you know what each line does. This is especially true if you've done anything beyond the basics when it comes to "collections" within your content<sup class="footnote-ref"><a href="#fn2" id="fnref2">[2]</a></sup>.</p>
<h3 id="3.-account-for-any-weirdness-in-your-posts" tabindex="-1">3. Account for any weirdness in your posts</h3>
<p>I think it's important that the articles I publish on my site are accessible by RSS. But sometimes RSS <em>isn't</em> the best tool for the job. I have several posts that rely on interactive elements within the page (most recently, my <a href="https://tomhazledine.com/web-audio-delay">Web Audio API</a> article includes plenty of illustrative web-audio-dependent examples), and these simply don't translate to a text-and-image-only medium.</p>
<p>I've sidestepped this problem by adding a <code>not_rss_friendly</code> flag to the frontmatter of these articles. When that flag is <code>true</code>, my RSS feed adds a caveat to the top of the content block. This allows me to explain that some of the content may not work on the reader's tool of choice, and they might want to check out the original web version. And what's more, this has no effect whatsoever on my main site; these caveats <em>only</em> appear in the RSS version.</p>
<h3 id="4.-validate-and-publish" tabindex="-1">4. Validate and publish</h3>
<p>The feed is easy enough to test locally. I have my Eleventy dev server pointing at <code>http://localhost:1337</code>, so to view my new feed I can look at <code>http://localhost:1337/feed.xml</code> to ensure everything has built as expected.</p>
<p>With a format as pernickety as XML, it's worth doing some proper validation, too. To test my local version I copy/pasted <code>view-source:http://localhost:1337/feed.xml</code> into <a href="https://validator.w3.org/feed/">the W3C validator</a>, which caught a few minor issues for me (it also flagged issues with non-matching URLs between local and live which I was happy to ignore at this stage).</p>
<p>For added pre-deployment comfort I turned to one of my favourite features of Netlify (the service I use to build and host my website). Their <a href="https://www.netlify.com/blog/2016/07/20/introducing-deploy-previews-in-netlify/">Deploy Previews</a> are a fantastic tool. Every time I create a PR on my GitHub repo, Netlify spools up a matching development server with a real URL. These previews are full, working versions of my site, and because the preview URL is a proper live domain on the actual internet I can submit that <em>preview</em> version of my feed into any feed-reader app and see a real version of what my changes will look like.</p>
<p>And because Netlify is just the best tool ever, all I need to do to deploy my new feed is to merge my PR. Boom. Nice and easy.<sup class="footnote-ref"><a href="#fn3" id="fnref3">[3]</a></sup></p>
<p>With that done, my new feed is live at <a href="https://tomhazledine.com/feed.xml">tomhazledine.com/feed.xml</a>. If you like to consume your content using RSS, you can add that feed to your reader of choice and never miss a post from this site (lucky you!).</p>
<h2 id="my-full-feed.md-template-file%3A" tabindex="-1">My full <code>feed.md</code> template file:</h2>
<pre><code class="language-xml"><?xml version="1.0" encoding="utf-8"?>
<feed xmlns="http://www.w3.org/2005/Atom">
<title>{{ site.title }}</title>
<subtitle>{{ site.summary }}</subtitle>
<link href="{{ site.url }}/{{ permalink }}" rel="self"/>
<link href="{{ site.url }}"/>
<updated>{{ collections.articles | getNewestCollectionItemDate | dateToRfc3339 }}</updated>
<id>{{ site.url }}/</id>
<author>
<name>{{ site.author }}</name>
<email>{{ site.authorEmail }}</email>
</author>
{%- for post in collections.articles | reverse %}
{% set absolutePostUrl %}{{ post.url | url | absoluteUrl(site.url) }}{% endset %}
<entry>
<title>{{ post.data.title }}</title>
<link href="{{ absolutePostUrl }}"/>
<updated>{{ post.date | dateToRfc3339 }}</updated>
<id>{{ absolutePostUrl }}</id>
<content type="html">
{%- if post.data.not_rss_friendly %}
{{ site.rssCaveat | markdown }}<p>View the original here: <a href="{{ absolutePostUrl }}">{{ absolutePostUrl }}</a></p>
{%- endif %}
{{ post.templateContent | markdown | htmlToAbsoluteUrls(absolutePostUrl) }}
</content>
</entry>
{%- endfor %}
</feed>
</code></pre>
<hr class="footnotes-sep"/>
<section class="footnotes">
<ol class="footnotes-list">
<li id="fn1" class="footnote-item"><p>Before we start, let's have a moment's silence for the passing of Google Reader. I know at this point it's been over seven years since it was so cruelly taken from us, but I'm still a bit cross about it. <a href="#fnref1" class="footnote-backref">↩︎</a></p>
</li>
<li id="fn2" class="footnote-item"><p>I've got to be honest, even when using Eleventy's "debug" mode this was a hard template to put together. Becasue there are various level of content-escaping going on, I became very familiar with the <em>"Opening and ending tag mismatch"</em> error message. <a href="#fnref2" class="footnote-backref">↩︎</a></p>
</li>
<li id="fn3" class="footnote-item"><p>Now all that's left to do is decide where to put the feed-link icon... 🤔 <a href="#fnref3" class="footnote-backref">↩︎</a></p>
</li>
</ol>
</section>Building a delay effect with the Web Audio API2020-12-30T00:00:00Zhttps://tomhazledine.com/web-audio-delay/<p>In this post I'm going to show you how to use JavaScript to recreate a delay pedal. A delay pedal is something that you'd place between a guitar and an amplifier to add a delay effect to the signal from the guitar. They're a relatively common piece of musical kit, and I'm going to recreate one in the browser.</p><h2 id="the-audio-context">The audio context</h2><p>To get started, we will need an <em>audio context</em>. The audio context is our gateway into the audio power of a browser.</p><pre><code class="hljs language-js"><span class="hljs-keyword">const</span> context = <span class="hljs-keyword">new</span> <span class="hljs-variable language_">window</span>.<span class="hljs-title class_">AudioContext</span>();
</code></pre><p>Having created a new <code>AudioContext</code> object (which in this instance we're naming "context"), we can then use that context object to do all sorts of things: we can create audio sources, connect audio-creating-things to audio-listening-things, all of that good stuff.</p><p>And of course because we live in the real world we need to fudge things a little to make sure that it works in as many different browsers as possible:</p><pre><code class="hljs language-js"><span class="hljs-keyword">const</span> context = <span class="hljs-keyword">new</span> (<span class="hljs-variable language_">window</span>.<span class="hljs-property">AudioContext</span> || <span class="hljs-variable language_">window</span>.<span class="hljs-property">webkitAudioContext</span>)();
</code></pre><p>Once we've got that <code>AudioContext</code> set up, we can then start using it. And the way that we're going to use it is to recreate the idea of an analogue <em>signal path</em>, but in code.</p><h2 id="the-signal-path">The signal path</h2><p>A good example of a "signal path" is a how a band sets up on stage. It's the path that sound takes from an instrument to a mixing desk and then out to some speakers (so the audience can hear it).</p><div class="signal-path"><div class="signal-path__labels"><div class="signal-path__label">Instrument</div><div class="signal-path__label">Mixer</div><div class="signal-path__label">Speakers</div></div></div><p>And in audio-context terms, <strong>Instrument</strong> > <strong>Mixer</strong> > <strong>Speakers</strong>
translates to <code>oscillator</code> > <code>gainNode</code> > <code>context.destination</code></p><ol><li>The instrument becomes an <code>oscillator</code> (we'll be using an oscillator to create some sounds)</li><li>The mixer will become a <code>gainNode</code> (which is a way for us to control the level of that signal - the "volume", if you will)</li><li>The speakers will become our <code>context.destination</code></li></ol><p>Now let's work backwards through this path in more detail...</p><h3 id="3-the-destination">3. The destination</h3><p>The <code>context.destination</code> is available on the <code>context</code> object that we created, and the "destination" will be either the speakers in your laptop or your headphones or some kind of Bluetooth connection. In short: wherever your sound goes when it leaves your computer, that'll be your context's "destination".</p><h3 id="2-gain">2. Gain</h3><p>The <code>gainNode</code> is a little bit more complicated. This is a way of controlling the "volume" of our signal. We'll use our <code>context</code> object to create a "gain node". We'll call the new node "master" (because it will act as our master volume control), and we'll need to set a value for it.</p><pre><code class="hljs language-js"><span class="hljs-keyword">const</span> master = context.<span class="hljs-title function_">createGain</span>();
master.<span class="hljs-property">gain</span>.<span class="hljs-property">value</span> = <span class="hljs-number">0.8</span>;
master.<span class="hljs-title function_">connect</span>(context.<span class="hljs-property">destination</span>);
</code></pre><p>The sound comes in at a raw level, essentially (if it comes from an oscillator it's just coming in at the default level), but once it gets to a gain node we can set what level we want that signal to be. The range runs from <code>0</code> to <code>1</code>, and in our example we're using <code>0.8</code> (just slightly less than full volume). Then on the last line we're connecting the <code>master</code> gain node to our <code>destination</code>, which will be the the <em>output</em> of the sound.</p><p>So we've got the sound coming in, we're setting that volume level with a <code>gain</code> node, and then connecting it with <code>.connect</code> (which is a method on all audio objects that we create from our <code>context</code>).</p><h3 id="1-oscillator">1. Oscillator</h3><p>Moving to the very start of that signal path (where the sound actually comes from!), we're going to simulate a "voltage-controlled oscillator" (a.k.a. a "VCO"). Of course it's <em>not</em> voltage controlled because we're not in the analogue world and we're not actually plugging electricity into things and wiring things up and soldering them. But we can <em>simulate</em> a VCO with the context's <code>createOscillator()</code> method.</p><pre><code class="hljs language-js"><span class="hljs-keyword">const</span> <span class="hljs-variable constant_">VCO</span> = context.<span class="hljs-title function_">createOscillator</span>();
<span class="hljs-variable constant_">VCO</span>.<span class="hljs-property">frequency</span>.<span class="hljs-property">value</span> = <span class="hljs-number">440.0</span>;
<span class="hljs-variable constant_">VCO</span>.<span class="hljs-title function_">connect</span>(master);
</code></pre><p>That creates an oscillator object. The object has has a frequency value which represents the <em>pitch</em> of the note that will be created. Here we're setting a value of <code>440</code>, which translates to 440 hertz (Hz). That is the frequency value of "Middle A", which in an orchestral setup is the note that everyone tunes to.</p><p>We're then connecting that to <code>master</code> (the gain node that we're treating as our "master volume"), which then sends our signal off to the to the destination.</p><p>Now that we've created an oscillator we can test that it's working using two simple methods: <code>start</code> and <code>stop</code>.</p><pre><code class="hljs language-js"><span class="hljs-comment">// Start the oscillator</span>
<span class="hljs-variable constant_">VCO</span>.<span class="hljs-title function_">start</span>();
<span class="hljs-comment">// Stop the oscillator</span>
<span class="hljs-variable constant_">VCO</span>.<span class="hljs-title function_">stop</span>();
</code></pre><p>Putting these methods into action might look a little like this:</p><pre><code class="hljs language-js"><span class="hljs-keyword">let</span> buttonState = <span class="hljs-string">"off"</span>;
<span class="hljs-keyword">const</span> button = <span class="hljs-variable language_">document</span>.<span class="hljs-title function_">querySelector</span>(<span class="hljs-string">"#our-button"</span>);
button.<span class="hljs-title function_">addEventListener</span>(<span class="hljs-string">"click"</span>, <span class="hljs-function">() =></span> {
<span class="hljs-keyword">if</span> (buttonState === <span class="hljs-string">"off"</span>) {
<span class="hljs-variable constant_">VCO</span>.<span class="hljs-title function_">start</span>();
buttonState = <span class="hljs-string">"on"</span>;
} <span class="hljs-keyword">else</span> {
<span class="hljs-variable constant_">VCO</span>.<span class="hljs-title function_">stop</span>();
buttonState = <span class="hljs-string">"off"</span>;
}
});
</code></pre><p>You can test this out using the button below. Clicking once will call <code>.start()</code>, and you should hear a (probably quite horrible) noise. Then clicking a second time will call <code>.stop()</code>, which will end the sound.</p><div id="delay-block-one"/><p>If we look at the frequency graph of what's happening, you can see when we start the pitch we get a peak at 440Hz, which is the "middle A" that we are hoping for.</p><p>This code gives us a constant tone which is not massively useful for adding a delay to. The delays will start layering up and overlapping, so we're not going to be able to hear what's going clearly. What will be more useful for us is to have a <em>pulse</em>, so let's look at how to have the button trigger a pulse of sound:</p><pre><code class="hljs language-js"><span class="hljs-comment">// Start the oscillator now</span>
<span class="hljs-variable constant_">VCO</span>.<span class="hljs-title function_">start</span>(context.<span class="hljs-property">currentTime</span>);
<span class="hljs-comment">// Stop the oscillator in .25 seconds time</span>
<span class="hljs-variable constant_">VCO</span>.<span class="hljs-title function_">stop</span>(context.<span class="hljs-property">currentTime</span> + <span class="hljs-number">0.25</span>);
</code></pre><p>Now when we hit "start" we're not just a calling <code>.start()</code> with no parameters: we're calling it with our current time and then straight away calling the <code>.stop()</code> function with a different time. This means our code will know to start and stop at these given times.</p><p>We're getting time from <code>context.currentTime</code>, which is a more reliable way of getting a time-value than using a <code>setTimeout()</code> like we would normally use in JavaScript. The audio context has its own internal clock which is much more precise.</p><div id="delay-block-two"/><h2 id="controlling-a-vco-with-a-gain-node-aka-a-vca">Controlling a VCO with a gain node (a.k.a. a VCA)</h2><p>So now we get a pulse of sound that lasts for 0.25 seconds, but it's still kind of sudden. What we will probably ought to do now is "soften" this slightly so it's not quite such a jarring sound. It just turns on and off. What we want is to have a way of ramping up the volume gradually and then ramping that down again.</p><p>We want it to fade in and fade out, and we can achieve that by using <em>another</em> gain node.</p><p>We've already used a gain node for our master volume, and now we're using a new gain node paired just to our oscillator.</p><pre><code class="hljs language-js"><span class="hljs-keyword">const</span> note = {
<span class="hljs-attr">vco</span>: context.<span class="hljs-title function_">createOscillator</span>(),
<span class="hljs-attr">vca</span>: context.<span class="hljs-title function_">createGain</span>()
};
note.<span class="hljs-property">vco</span>.<span class="hljs-title function_">connect</span>(note.<span class="hljs-property">vca</span>);
note.<span class="hljs-property">vca</span>.<span class="hljs-title function_">connect</span>(master);
<span class="hljs-keyword">const</span> button = <span class="hljs-variable language_">document</span>.<span class="hljs-title function_">querySelector</span>(<span class="hljs-string">"#our-button"</span>);
button.<span class="hljs-title function_">addEventListener</span>(<span class="hljs-string">"click"</span>, <span class="hljs-function">() =></span> {
<span class="hljs-comment">// Start the oscillator gradually</span>
note.<span class="hljs-property">vca</span>.<span class="hljs-property">gain</span>.<span class="hljs-title function_">exponentialRampToValueAtTime</span>(<span class="hljs-number">1</span>, context.<span class="hljs-property">currentTime</span> + <span class="hljs-number">0.2</span>);
<span class="hljs-comment">// Stop the oscillator gradually</span>
note.<span class="hljs-property">vca</span>.<span class="hljs-property">gain</span>.<span class="hljs-title function_">exponentialRampToValueAtTime</span>(
<span class="hljs-number">0.0001</span>,
context.<span class="hljs-property">currentTime</span> + <span class="hljs-number">0.5</span>
);
});
</code></pre><p>Here we've made a regular JavaScript object called <code>note</code>, and we've given it a VCO and a VCA.</p><p>Whereas a VCO is a voltage controlled <em>oscillator</em>, a VCA is a voltage controlled <em>amplitude</em>. That's analogue-synthesiser-speak for a node that controls "volume". Here our VCA is going to be a gain node.</p><p>We connect those two together, then we set the values using a function called <code>exponentialRampToValueAtTime()</code> which, surprise surprise, exponentially ramps to a value at a
given time.</p><p>We want to ramp up to the volume value of <code>1</code> (a.k.a. maximum volume) and we want that to take <code>0.2</code> seconds. To stop the pulse, we then ramp all the way down to a value of <code>0</code> half a second later.</p><div id="delay-block-three"/><p>Now our pulse is starting to sound a little bit more sonorous. We've got a gentle pulse that more closely mimics sound that we'd hear in the real world (rather than the artificial on/off of the raw oscillator).</p><h2 id="randomly-selecting-a-note">Randomly selecting a "note"</h2><p>Now we can spice things up a little by randomly choosing the pitch of the note that is generated.</p><pre><code class="hljs language-js"><span class="hljs-keyword">const</span> <span class="hljs-title function_">getRandomInt</span> = (<span class="hljs-params">min, max</span>) =>
<span class="hljs-title class_">Math</span>.<span class="hljs-title function_">floor</span>(<span class="hljs-title class_">Math</span>.<span class="hljs-title function_">random</span>() * (max - min + <span class="hljs-number">1</span>)) + min;
note.<span class="hljs-property">frequency</span>.<span class="hljs-property">value</span> = <span class="hljs-title function_">getRandomInt</span>(<span class="hljs-number">220</span>, <span class="hljs-number">880</span>);
</code></pre><p>The <code>getRandomInt()</code> function creates a random integer between <code>220</code> and <code>880</code> (which is the frequency range, in hertz, that we want to hear). Now every time we press the "pulse" button we will hear a random pitch.</p><div id="delay-block-four"/><p>We talked about it being a bit more sonorous earlier, but because these pitches are random there is no sense of <em>musicality</em>. To enhance this a little bit we can make use of an array of note values that each have a <code>name</code> (so that we know what note we're dealing with) and a <code>value</code> (which is the
important part).</p><pre><code class="hljs language-js"><span class="hljs-keyword">const</span> C_Maj = [
{ <span class="hljs-attr">name</span>: <span class="hljs-string">"C4"</span>, <span class="hljs-attr">value</span>: <span class="hljs-number">261.63</span> },
{ <span class="hljs-attr">name</span>: <span class="hljs-string">"D4"</span>, <span class="hljs-attr">value</span>: <span class="hljs-number">293.66</span> },
{ <span class="hljs-attr">name</span>: <span class="hljs-string">"E4"</span>, <span class="hljs-attr">value</span>: <span class="hljs-number">329.63</span> },
{ <span class="hljs-attr">name</span>: <span class="hljs-string">"F4"</span>, <span class="hljs-attr">value</span>: <span class="hljs-number">349.23</span> },
{ <span class="hljs-attr">name</span>: <span class="hljs-string">"G4"</span>, <span class="hljs-attr">value</span>: <span class="hljs-number">392.0</span> },
{ <span class="hljs-attr">name</span>: <span class="hljs-string">"A4"</span>, <span class="hljs-attr">value</span>: <span class="hljs-number">440.0</span> },
{ <span class="hljs-attr">name</span>: <span class="hljs-string">"B4"</span>, <span class="hljs-attr">value</span>: <span class="hljs-number">493.88</span> },
{ <span class="hljs-attr">name</span>: <span class="hljs-string">"C5"</span>, <span class="hljs-attr">value</span>: <span class="hljs-number">523.25</span> },
{ <span class="hljs-attr">name</span>: <span class="hljs-string">"D5"</span>, <span class="hljs-attr">value</span>: <span class="hljs-number">587.33</span> },
{ <span class="hljs-attr">name</span>: <span class="hljs-string">"E5"</span>, <span class="hljs-attr">value</span>: <span class="hljs-number">659.26</span> },
{ <span class="hljs-attr">name</span>: <span class="hljs-string">"F5"</span>, <span class="hljs-attr">value</span>: <span class="hljs-number">698.46</span> },
{ <span class="hljs-attr">name</span>: <span class="hljs-string">"G5"</span>, <span class="hljs-attr">value</span>: <span class="hljs-number">783.99</span> }
];
</code></pre><p>Now that we have a list of all the notes in a <strong>C major</strong> scale, we can use that same random number generator. This time, however, we'll use it to generate a <code>key</code> for the array of notes.</p><pre><code class="hljs language-js"><span class="hljs-keyword">const</span> noteNumber = <span class="hljs-title function_">getRandomInt</span>(<span class="hljs-number">0</span>, C_Maj.<span class="hljs-property">length</span> - <span class="hljs-number">1</span>);
note.<span class="hljs-property">frequency</span>.<span class="hljs-property">value</span> = C_Maj[noteNumber].<span class="hljs-property">value</span>;
</code></pre><p>Now we are still randomly creating notes, but they're all within the bounds of a C major scale (which should in theory sound a little bit nicer). It's still a bit random but it is within the bounds of a normal scale so it sounds like of how we'd expect <em>music</em> to sound.</p><div id="delay-block-five"/><h2 id="doubling-up-our-note">Doubling-up our note</h2><p>Next, let's get even more fancy and duplicate the notes for a "fuller" sound. We can create a <em>second</em> VCO and VCA for our note:</p><pre><code class="hljs language-js"><span class="hljs-keyword">const</span> note = {
...note,
<span class="hljs-attr">vco2</span>: context.<span class="hljs-title function_">createOscillator</span>(),
<span class="hljs-attr">vca2</span>: context.<span class="hljs-title function_">createGain</span>()
};
</code></pre><p>And rather than manually choosing the note value we can "transpose" the value of our first VCO.</p><pre><code class="hljs language-js"><span class="hljs-keyword">const</span> <span class="hljs-title function_">transpose</span> = (<span class="hljs-params">freq, steps</span>) => freq * <span class="hljs-title class_">Math</span>.<span class="hljs-title function_">pow</span>(<span class="hljs-number">2</span>, steps / <span class="hljs-number">12</span>);
<span class="hljs-keyword">const</span> startingPitch = note.<span class="hljs-property">vco1</span>.<span class="hljs-property">frequency</span>.<span class="hljs-property">value</span>;
note.<span class="hljs-property">vco2</span>.<span class="hljs-property">frequency</span>.<span class="hljs-property">value</span> = <span class="hljs-title function_">transpose</span>(startingPitch, <span class="hljs-number">7</span>);
note.<span class="hljs-property">vco2</span>.<span class="hljs-title function_">connect</span>(note.<span class="hljs-property">vca2</span>);
note.<span class="hljs-property">vca2</span>.<span class="hljs-title function_">connect</span>(master);
</code></pre><p>This <code>transpose()</code> function takes in a frequency and the number of "steps" in either direction that we want to go. Say you want to transpose from a C to a D; that's two semitones (musical steps) up.</p><p>What the transpose function does is work that out for us. It calculates how we should mathematically transform our starting pitch, and does this by multiplying the frequency by a power law.</p><p>By adding a second VCO, we are doubling up the notes that we hear when we hit the "pulse" button. But rather than simply duplicating the sound, we're transposing the second VCO to be seven steps above the pitch of the first VCO. Seven steps up equates to a "fifth" in musical terminology (there are 12 <em>steps</em> but eight <em>notes</em>, so seven steps up is actually five notes up the scale).</p><p>You can see see the double peak on the frequency graph when we trigger a pulse:</p><div id="delay-block-six"/><h2 id="the-fx-loop">The FX loop</h2><p>Let's get to the business at hand of actually adding in our effects unit.</p><p>This will be a unit that slots into our signal path and adds an effect to the signal.</p><p>The "normal" signal path (as we've already established) is "instrument" -> "mixer" -> "speaker". The FX ("effects", for non-nerds) loop sits into that path between the instrument and the mixer.</p><p>{% include "delay/signalpath-fx.njk" %}</p><p>This little module takes a bit of the signal, does something to it, and then sends that
signal back up to the mixer along with the original signal itself.</p><p>Creating a delay object in JavaScript is nice and easy to do because the audio <code>context</code> has the concept of "delay" built into it.</p><pre><code class="hljs language-js"><span class="hljs-keyword">const</span> delay = context.<span class="hljs-title function_">createDelay</span>();
delay.<span class="hljs-property">delayTime</span>.<span class="hljs-property">value</span> = <span class="hljs-number">0.4</span>;
delay.<span class="hljs-title function_">connect</span>(master);
</code></pre><p>The <code>delayTime</code> value determines how much time the <code>delay</code> node will delay the signal
before sending it on again.</p><p>Anything that we connect to that <code>delay</code> will then be paused for whatever value we set (<code>0.4</code> seconds, in this example) before then being sent on to the master output.</p><p>If we hook our notes' VCOs to the delay node, then our pulse will be heard twice (audio nodes can have multiple connections, so here our notes are connected directly to the <code>master</code> output <em>and</em> indirectly via the <code>delay</code> node).</p><pre><code class="hljs language-js">note.<span class="hljs-property">vca1</span>.<span class="hljs-title function_">connect</span>(master);
note.<span class="hljs-property">vca2</span>.<span class="hljs-title function_">connect</span>(master);
note.<span class="hljs-property">vca1</span>.<span class="hljs-title function_">connect</span>(delay);
note.<span class="hljs-property">vca2</span>.<span class="hljs-title function_">connect</span>(delay);
delay.<span class="hljs-title function_">connect</span>(master);
</code></pre><div id="delay-block-seven"/><p>Success! We have now created a delay. But we don't have the full picture yet; this is only applying the delay once. When we press the "pulse" button, we hear the original note and a single repetition of that note forty milliseconds (<code>0.4</code> seconds) later.</p><h2 id="fx-feedback">FX Feedback</h2><p>What we're missing is the idea of "feedback". Feedback is the mechanism by which a small part of the delayed signal is <em>fed back</em> into the delay module.</p><p>{% include "delay/fx-loop.njk" %}</p><p>We could connect the delay node to itself directly (<code>delay.connect(delay)</code>), but this would send the <em>entire</em> signal back into the delay. This would be an infinite loop, so let's not do that because it would sound terrible.</p><p>What we're going to do is create a new gain node. We've used a lot of game nodes so far, so we're pretty familiar with them at this point.</p><p>We eliminate the problem of infinite feedback by giving the <code>feedback</code> node a very small value: <code>0.3</code> (which is equivalent to saying "30% of the signal"). Then we put this new <code>feedback</code> node in between the delay output and the delay input</p><pre><code class="hljs language-js"><span class="hljs-keyword">const</span> feedback = context.<span class="hljs-title function_">createGain</span>();
feedback.<span class="hljs-property">gain</span>.<span class="hljs-property">value</span> = <span class="hljs-number">0.3</span>;
delay.<span class="hljs-title function_">connect</span>(feedback);
feedback.<span class="hljs-title function_">connect</span>(delay);
delay.<span class="hljs-title function_">connect</span>(master);
</code></pre><div id="delay-block-eight"/><p>The amount of signal that we're passing back in controls how many times we hear the pulse repeated; that's the audible delay. The combination of the delay <em>duration</em> (<code>delay.delayTime.value</code>) and the level of the <em>feedback</em> (<code>feedback.gain.value</code>) combine to create the full effect.</p><h2 id="making-it-interactive">Making it interactive</h2><p>There's one final step required to fully mimic the behaviour of a "real" delay pedal. To finish off, I'll plug the two crucial factors (the duration and the feedback) into some HTML range elements. This allows us to tweak those values "on the fly". The higher the feedback level, the longer the repetition of the pulse will continue for. The longer the delay duration, the larger the gaps between the repetitions will be.</p><p>Have a play yourself, and see what crazy sounds you can make. You can even set the feedback level to <code>100%</code>, but be warned - weird things will happen!</p><div id="delay-block-nine"/><hr/><p>This post has, hopefully, shown you some of the power of the audio context as it exists in the browser. We've just scratched the surface; creating a tone and manipulating it in small ways is pretty simple stuff, but there is no end to the amount of creativity that can be had from playing with these tools. I hope this has inspired you to do something of your own.</p><p>If you've enjoyed this or you have questions please do get in touch on Twitter and let me know if you've done something crazy with the audio context. I'd love to see what you've come up with.</p>Dark mode: hard mode2020-11-19T00:00:00Zhttps://tomhazledine.com/dark-mode/<p>Dark mode has been around in operating systems and web browsers for a while now, but it wasn't something I'd ever really cared about. But that all changed when my brother implemented a dark-mode stylesheet for <a href="https://edthecoder.dev/">his (seldom updated but excellent) blog, edthecoder.dev</a>. In a true spirit of <em>"anything you can do, I can do better"</em>, the sibling rivalry kicked in and I knew I had to implement dark mode on my own site. If a "mere" backend dev could implement a solid dark mode, surely a "frontend expert" such as myself should be able to do the same (but better).</p>
<ol>
<li><a href="#the-basics">The basics</a>: simple dark-mode with the <code>prefers-color-scheme</code> media query.</li>
<li><a href="#all-in-one-place">All in one place</a>: defining dynamic themes with CSS custom properties.</li>
<li><a href="#live-switching">Live switching</a>: toggling dark-mode without having to change a system-wide preference.</li>
</ol>
<hr/>
<h2 id="the-basics" tabindex="-1">The basics</h2>
<p>At its simplest, <strong>"dark mode"</strong> can be applied to your stylesheet by using the <code>prefers-color-scheme</code> media query. This behaves in much the same way as more familiar size-related queries (for example <code>@media (min-width: 960px) {}</code>), and will only apply the styles within it if the browser detects that the system has the <code>prefers-color-scheme: dark</code> flag set.</p>
<pre><code class="language-css">.thing {
/* "normal" colour rules */
color: black;
background: white;
@media (prefers-color-scheme: dark) {
/* These colour rules will only be
applied if the site is loaded on a
machine with dark-mode enabled */
color: white;
background: black;
}
}
</code></pre>
<hr/>
<h2 id="all-in-one-place" tabindex="-1">All in one place</h2>
<p>I've previously been declaring the colours in my stylesheet using <code>scss</code> variable (for example, <code>$black: #4d4d4d</code>). This would be fine if I was happy adding <code>@media</code> rules to handle dark-mode throughout my stylesheet, but that comes with a few issues:</p>
<ul>
<li>The dark-mode specific rules would be spread all over the place, introducing unnecessary complexity and creating a maintenance headache.</li>
<li>Sass variables are not dynamic (a.k.a. they are defined once, at compilation time), and therefore wouldn't be able to adjust on-the-fly depending on the light/dark setting of the client.</li>
</ul>
<p>What I need are colour variables that can change value depending on their context. What I need are <a href="https://developer.mozilla.org/en-US/docs/Web/CSS/--*">CSS Custom Properties</a>.</p>
<h3 id="css-custom-properties" tabindex="-1">CSS Custom Properties</h3>
<p>Custom Properties are native CSS variables, and they are <a href="https://caniuse.com/?search=custom%20properties">supported in all major browsers</a> (RIP in peace, IE). Unlike Sass variables, they are evaluate by the client at runtime. This means that our stylesheet can adapt to whatever mode the client is using (light or dark), and we could even change that setting dynamically with JavaScript.</p>
<blockquote>
<p><em><strong>Note:</strong> Unlike variables in <code>scss</code>, custom properties need to be declared inside a wrapper. Common practice is to use the <code>:root</code> pseudo class to ensure the custom properties are accessible in the entire cascade (<a href="https://developer.mozilla.org/en-US/docs/Web/CSS/:root"><code>:root</code> represents the <html/> element and is identical to the selector html, except that its specificity is higher</a>).</em></p>
</blockquote>
<pre><code class="language-css">/* a static Sass variable */
$primary: red;
/* a dynamic custom property */
:root {
--primary: red;
}
.unchanging_thing {
/* This value is fixed when the stylesheet is compiled */
color: $primary;
}
.changing_thing {
/* This value will change on-the-fly if --primary is changed */
color: var(--primary);
}
</code></pre>
<p>For a long time now, I've declared all colour variables in a single <code>scss</code> file (<code>_settings.colours.scss</code>, in case you were wondering) which makes the job of maintaining a unified "theme" across a complex site a relatively simple task. Having to spread <code>prefers-color-scheme</code> queries throughout the various parts of the stylesheet messes this up big time.</p>
<p>Using custom properties allows us to abstract the theme-specific parts of the stylesheet away from specific elements. Crucially, it allows us to add a single <code>prefers-color-scheme</code> query into the existing <code>colours.scss</code> partial. Nice and tidy!</p>
<pre><code class="language-css">/* Default colours */
:root {
--primary: red;
--secondary: blue;
/* etc... */
@media (prefers-color-scheme: dark) {
/* Dark-theme colour alternatives */
--primary: maroon;
--secondary: navy;
/* etc... */
}
}
</code></pre>
<h3 id="gotchas-when-using-custom-properties-and-sass" tabindex="-1">Gotchas when using custom properties <em>and</em> Sass</h3>
<p>Something to watch out for when using custom properties inside Sass is that things get weird when you try to use them inside functions. Sass is evaluated when the file is complied, so things like <code>darken($colourVar, 10%)</code> are set in stone as soon as you hit save (and therefore not dynamic when the file is loaded by a browser). Using a custom property in this instance will result in an error, and the <code>scss</code> will not compile.</p>
<p>If you're locked-in to (pseudo-)dynamically adjusting colour values, then <a href="https://developer.mozilla.org/en-US/docs/Web/CSS/filter">CSS' native <code>filter</code> rule</a> might be an option for you. Because <code>filter</code> adjusts the entire element, I find it much simpler to manually set the colour value I want. So rather than using <code>$red: #ff0000;</code> and <code>$red--dark: darken(red, 20%);</code>, we would need to write <code>--red: #ff0000;</code> and <code>--red--dark: #990000;</code></p>
<p>Another thing to watch out for is how specific you need to be when formatting colours. With Sass variables, I've grown accustomed to being able to declare a colour as a hex value and have it "just work" wherever I want to use it. I could declare a variable as <code>#ff0000</code> and use that variable inside an <code>rgba()</code> function to apply opacity: <code>rgba($red, 0.3)</code>. In that instance, rather than having to convert my hex colour into the RGB equivalent (<code>255, 0, 0</code>), Sass would do the conversion work for me.</p>
<p>With native CSS custom properties, we don't have this convenience.</p>
<pre><code class="language-css">/* Works in Sass */
$red: #ff0000;
$red--translucent: rgba($red, 0.3);
/* Does not work in Sass (or anywhere else) */
--red: #ff0000;
--red--translucent: rgba(--red, 0.3);
/* Works everywhere */
--red: #ff0000;
--red--translucent: rgba(255, 0, 0, 0.3);
</code></pre>
<p>This meant that I had to do a lot of hex-to-rgb conversion when converting my stylesheet to use custom properties. Turns out I was a heavy user of both RGBA colours <em>and</em> declared all my colours as hex values. Oops.</p>
<hr/>
<h2 id="live-switching" tabindex="-1">Live switching</h2>
<p>By doing all that we've done so far, we've successfully implemented dark mode! After a <s>trivial</s> <em>long and hard</em> refactor I've now reached feature-parity with my brother's site. But the goal here was to <em>surpass</em> Ed's efforts, and the best way I can think of to achieve that is really lean in to the dynamic aspect of the custom properties I've taken such pains to add.</p>
<p>What we've done so far is to switch colour-theme based on the <code>prefers-color-scheme</code> value, but that's not the only thing we can use to toggle between themes. With our custom properties in place, I could add as many "themes" as I want. For now I'm happy with "light mode" and "dark mode", but to prove the concept it would be great to be able to toggle between the modes on-the-fly. Currently, unless a visitor to the site already has dark-mode enabled on their system, there's no way for them to know that the dark version of the site even exists.</p>
<p>Allowing users to dynamically toggle between themes has a few requirements:</p>
<ul>
<li>We'll need to detach the theme-switching logic from the <code>prefers-color-scheme</code> state (but still respect that setting if it exists).</li>
<li>We'll need a UI element to activate the toggling, with display states for both themes.</li>
<li>We'll need to persist the user's setting between page loads (requiring them to re-set the value on every page load would be really obnoxious).</li>
</ul>
<h3 id="here-comes-the-javascript!" tabindex="-1">Here comes the JavaScript!</h3>
<p>The first step is easy. We can move the dark-mode custom properties out of the <code>@media</code> query, and use a <a href="https://developer.mozilla.org/en-US/docs/Learn/HTML/Howto/Use_data_attributes">data attribute</a> instead:</p>
<pre><code class="language-css">:root {
--primary: #00b7c6;
/* Normal colours... */
}
:root[data-theme="dark"] {
--primary: #ff851b;
/* Dark-mode colours... */
}
</code></pre>
<p>The next step is to create a JavaScript function to handle changing that data attribute. This <code>setDarkMode()</code> function will accept a boolean parameter that decides if we're applying <em>light</em> or <em>dark</em> mode. We'll also set a <code>localStorage</code> value so that the page remembers which mode the user has chosen.</p>
<pre><code class="language-js">const setDarkMode = (active = false) => {
const wrapper = document.querySelector(":root");
if (active) {
wrapper.setAttribute("data-theme", "dark");
localStorage.setItem("theme", "dark");
} else {
wrapper.setAttribute("data-theme", "light");
localStorage.setItem("theme", "light");
}
};
</code></pre>
<p>With that function in place, we then need to run it when the page loads. This is where we need to detect the system preference (the JS equivalent of the CSS <code>prefers-color-scheme</code> value). We can do this with <a href="https://developer.mozilla.org/en-US/docs/Web/API/Window/matchMedia">the browser's <code>matchMedia()</code> method</a> - which returns the value of a given media query. So if we retrieve this value when the page loads, we can call our <code>setDarkMode()</code> function with the correct boolean. As an added bonus, we can attach an event listener to the query which will fire whenever the system setting changes (meaning that if someone switches dark mode on or off while the page is open, the change will be applied instantly).</p>
<pre><code class="language-js">const query = window.matchMedia("(prefers-color-scheme: dark)");
setDarkMode(query.matches);
query.addListener(e => setDarkMode(e.matches));
</code></pre>
<p>This is made a little more complicated when we account for the setting from <code>localStorage</code>, which we want to be able to override the system preference (this is the <em>user's</em> explicit choice, after all).</p>
<pre><code class="language-js">const query = window.matchMedia("(prefers-color-scheme: dark)");
const themePreference = localStorage.getItem("theme");
let active = query.matches;
if (themePreference === "dark") {
active = true;
}
if (themePreference === "light") {
active = false;
}
setDarkMode(active);
query.addListener(e => setDarkMode(e.matches));
</code></pre>
<p>But even with this plumbing set up and ready, we're still missing a key part of the puzzle: the ability for user's to actually set their preference on the page itself. For that, we'll need a <code><button></code> in our HTML and an event listener to handle to toggling of the theme.</p>
<pre><code class="language-js">const toggleDarkMode = () => {
const theme = document.querySelector(":root").getAttribute("data-theme");
// If the current theme is "light", we want to activate the dark theme
setDarkMode(theme === "light");
};
// ".js__dark-mode-toggle" is the unique class we've added to the <button>
const toggleButton = document.querySelector(".js__dark-mode-toggle");
toggleButton.addEventListener("click", toggleDarkMode);
</code></pre>
<h3 id="putting-it-all-together" tabindex="-1">Putting it all together</h3>
<p>The final piece of the puzzle is to add some CSS sparkle to the button element (I ended up settling for a cheesy single-div sun/moon combo), and then we've got all the pieces we need! You can see all this code in action live on this very site; click the icon in the top right of the screen to toggle the light/dark modes. You can also see the full code (and mess about with it to your heart's content) in the <a href="https://codepen.io/tomhazledine/pen/XWjJMPL">CodePen demo</a> embedded below:</p>
<p class="codepen" data-height="265" data-theme-id="dark" data-default-tab="css,result" data-user="tomhazledine" data-slug-hash="XWjJMPL" style="height: 265px; box-sizing: border-box; display: flex; align-items: center; justify-content: center; border: 2px solid; margin: 1em 0; padding: 1em;" data-pen-title="Dark Mode Toggle">
<span>See the Pen <a href="https://codepen.io/tomhazledine/pen/XWjJMPL">
Dark Mode Toggle</a> by Tom Hazledine (<a href="https://codepen.io/tomhazledine">@tomhazledine</a>)
on <a href="https://codepen.io">CodePen</a>.</span>
</p>
<script async="async" src="https://static.codepen.io/assets/embed/ei.js"/>Quirks mode2020-08-12T00:00:00Zhttps://tomhazledine.com/podcasts-for-nerds/15-quirks-mode/<p><em>Announcement: from this week onwards, Podcasts for Nerds is switching to a fortnightly format. I want to stress that this is because I'm lazy, and not because I think you're lazy. A lot of the feedback I'm getting is that you folks don't have enough time to listen to the recommendations - and that's fine! The whole premise of this newsletter is that you can cherrypick the shows that appeal to you. Don't feel bad about saying to me "I don't really listen to many of them, but that one that I did listen to was awesome!": that's literally the whole point of this newsletter.</em></p>
<h2 id="three-great-episodes-to-listen-to-this-week%3A" tabindex="-1">Three great episodes to listen to this week:</h2>
<dl>
<dt><a href="https://shoptalkshow.com/425/">Tailwind, Where to Find Inspiration, SVG Corrections, and Web Workers</a>, <em>ShopTalk Show</em> (55mins, 2020-08-10)</dt>
<dd>This is probably the podcast I've listened to for the longest. At first it was a great way to learn more about frontend development, but over time (as is often the way with podcasts) I've come to feel that I know the hosts. Now it feels like I'm just hanging out with friends. This feeling intensified this week when STS co-host Chris Coyier joined me and the li'l bro on a recording of the A Question of Code podcast. Now I know for sure that Chris <em>is</em> as fun to hang out with as he sounds. We've included a "special" episode of STS on this list before, but this week's episode (number 425!) is a better example of what STS is really all about: frontend web chat. <a href="https://shoptalkshow.com/425/">Find it here</a>.</dd>
<dt><a href="http://thewestwingweekly.com/episodes/101">1.01: PILOT</a>, <em>The West Wing Weekly</em> (46mins, 2016-03-22)</dt>
<dd>I considered <em>not</em> including TWWW in this newsletter because even though I'm a generic nerd, I'm aware this show is very much a particular quirk of mine. But it ended up being one of my all time favourite podcasts, and it was really successful so there must be plenty of other weirdos like me out there. The premise? Can you even say you've binge-watched an old series if you didn't consume the watchalong podcast too? The West Wing Weekly is just that: a discussion about an episode of the West Wing TV show, one a week from start to finish. I suspect you probably <em>do</em> have to be a fan of the West Wing to properly "get" the show, but I think this podcast is great and just gets better as it goes along. I binged all ~200 episodes in a couple of months (along with the obligatory WW re-watch, too). <a href="http://thewestwingweekly.com/episodes/101">Find it here</a>.</dd>
<dt><a href="https://gimletmedia.com/shows/heavyweight/brholm">Gregor</a>, <em>Heavyweight</em> (47mins, 2016-09-24)</dt>
<dd>Heavyweight is a very highly reviewed and exquisitely produced show, and I've only ever listened to one episode. I thought it was brilliant, and great example of non-fiction storytelling in audio - but I'm pretty sure I mostly liked it because of the premise. <em>"20 years ago, Gregor lent some CDs to a musician friend. The CDs helped make him a famous rockstar. Now, Gregor would like some recognition. But mostly, he wants his CDs back."</em> See? And it's better than it sounds, too. I doubt the other episodes are a strong as this, so I've avoided them. Reply to this email if you've heard the other episodes and think I'm wrong; I'd hate to be missing out on an awesome show. (Hang on, doesn't that completely reverse the point of this newsletter?! Oops.) <a href="https://gimletmedia.com/shows/heavyweight/brholm">Find it here</a>.</dd>
</dl>
<h2 id="not-a-podcast%2C-but..." tabindex="-1">Not a podcast, but...</h2>
<p>Uh oh - CGP Grey was wrong on the internet. He released a video with (in his opinion) a massive error. His video explaining and deconstructing how the mistake happened is actually more entertaining than the original video (which was quite interesting to start with). It's very "inside baseball", but you know by now that "inside baseball" is my favourite genre of content by far.* <a href="https://www.youtube.com/watch?v=ua4QMFQATco">Watch it here</a>.</p>
<p>As always, feel free to forward this email to someone you think might like it. And if you've been forwarded this email (by someone awesome, no doubt), you can <a href="https://podcastsfornerds.com/">sign up for yourself at <em>podcastsfornerds.com</em></a> to be sent all future issues.</p>
<p>Thanks again,
Tom.</p>
<p>* There is no <em>actual</em> baseball content in any of these recommendations.</p>Riding the modular wave2020-08-05T00:00:00Zhttps://tomhazledine.com/podcasts-for-nerds/14-riding-the-modular-wave/<p><em>I've noticed I have to make a conscious effort to not have every recommendation be music-related. Thanks to discovering the world of modular synthesis I've fallen in love with music and music making all over again, and this is reflected in the podcasts I'm listening to. Is that a bad thing? My hobbies often come and go in waves, so we'll probably swing back to another topic in a few weeks.</em></p>
<h2 id="three-great-episodes-to-listen-to-this-week%3A" tabindex="-1">Three great episodes to listen to this week:</h2>
<dl>
<dt><a href="https://podcasts.apple.com/nz/podcast/mahler-symphony-no-6-part-1/id1215386938?i=1000481753362">Mahler Symphony No. 6</a>, <em>Sticky Notes: The Classical Music Podcast</em> (42mins, 2020-07-02)</dt>
<dd>In this three-part series of <em>Sticky Notes</em>, we hear Mahler's 6th dissected and examined. I'm of the school of thought that believes knowing more about how a piece was made <em>deepens</em> the enjoyment of it, and that holds doubly true for the classical greats. Structure and technique are such fundamental parts of Mahler's music, and nowhere is that more apparent than in his 6th Symphony. <a href="https://podcasts.apple.com/nz/podcast/mahler-symphony-no-6-part-1/id1215386938?i=1000481753362">Find it here</a>.</dd>
<dt><a href="https://www.earwolf.com/episode/face-off-live/">Face Off: LIVE!</a>, <em>How Did This Get Made?</em> (94mins, YYYY-MM-DD)</dt>
<dd>There's a manic energy to all <em>HDTGM</em> episodes which can be "too much" way too easily. But when that mania is mirrored in the subject of the episode, the format really works and the energy becomes infectious. The tagline for this show is "Have you ever seen a movie so bad that it’s amazing?", and 1990's action blockbuster Face:Off absolutely fits that bill. <a href="https://www.earwolf.com/episode/face-off-live/">Find it here</a>.</dd>
<dt><a href="https://www.stitcher.com/podcast/national-public-radio/how-i-built-this/e/50511717">WeWork: Miguel McKelvey</a>, <em>How I Built This</em> (48mins, 2018-09-02)</dt>
<dd>With all the madness and hype surrounding WeWork's founder (and all-round bad-egg) Adam Neumann, it's easy to overlook that he was actually the <em>co</em>-founder. The actual seeds of the idea, and the domain expertise to make it happen, came from architect Miguel McKelvey. Whereas Neumann is more likely to appear in an episode of <em>Behind the Bastards</em> (we'll cover that show here soon, I'm sure), McKelvey's story is far more wholesome: it's a tale of a good idea executed well at the right time. WeWork has become, well, "controversial" of late, but the origin story is genuinely interesting. <a href="https://www.stitcher.com/podcast/national-public-radio/how-i-built-this/e/50511717">Find it here</a>.</dd>
</dl>
<h2 id="not-a-podcast%2C-but..." tabindex="-1">Not a podcast, but...</h2>
<p>Those of you following along with my modular-synth adventure (which has it's seeds in the early issues of this very newsletter) would be forgiven for thinking it's all bleeps and bloops and GAS (Gear Acquisition Syndrome). But at its core is the fact that modular makes making music <em>fun</em> again (for me, at least). Nowhere is that better articulated in this video from <em>Red Means Recording</em>. There are bleeps, for sure, but the driving force behind this video is the music. It's really well put together and I've watched it at least twenty times already. <a href="https://www.youtube.com/watch?v=Vfd-ViehdA0&t=1138s">Watch it here</a>.</p>
<p>As always, feel free to forward this email to someone you think might like it. And if you've been forwarded this email (by someone awesome, no doubt), you can <a href="https://podcastsfornerds.com/">sign up for yourself at <em>podcastsfornerds.com</em></a> to be sent all future issues.</p>
<p>Thanks again,
Tom.</p>Lots of things. No theme.2020-07-29T00:00:00Zhttps://tomhazledine.com/podcasts-for-nerds/13-lots-of-things-no-theme/<p><em>Bit of an eclectic mix this week, but I think that's fine. Sometimes have a unifying theme can be interesting, but more often than not it's just an arbitrary limitation. There's also been more big-money acquisition news in the world of podcasting this week... but hey, sometimes it's good to focus on enjoying the content rather than obsessing over the business model. (I will definitely return to obsessing over the business model next week 😛)</em></p>
<h2 id="three-great-episodes-to-listen-to-this-week%3A" tabindex="-1">Three great episodes to listen to this week:</h2>
<dl>
<dt><a href="https://podcasts.apple.com/us/podcast/got-it/id1455935823">Offbeat/Psychedelic</a>, <em>Got it</em> (3mins, 2020-01-17)</dt>
<dd>Another example of stretching the idea of what a podcast can be, this one is simply a two-person parlour game that's been recorded. Kind of fun to listen to, but mostly useful as inspiration to try it out myself next time I get a chance. Got it? <a href="https://podcasts.apple.com/us/podcast/got-it/id1455935823">Find it here</a>.</dd>
<dt><a href="https://www.stitcher.com/podcast/welcome-to-geektown/e/56027664">Wakandan Econ</a>, <em>Welcome to Geektown</em> (28mins, 2018-08-31)</dt>
<dd>If you've seen any of the recent Marvel films, you might be wondering if Wakanda (home of the Black Panther) could ever really exist. Could a super-secret but highly-developed country become the most technologically advanced nation on the planet? The podcast looks at that question from an economic perspective. This one might cross the line from "nerd" to "geek", but the inclusion of actual economic theory from an actual economics professor means it's something I'm comfortable including here. <a href="https://www.stitcher.com/podcast/welcome-to-geektown/e/56027664">Find it here</a>.</dd>
<dt><a href="https://talkpython.fm/episodes/show/270/python-in-supply-chains-oil-rigs-rockets-and-lettuce">Python in supply chains: oil rigs, rockets, and lettuce</a>, <em>Talk Python</em> (122mins, 2020-06-25)</dt>
<dd>This week my brother Ed and I had the pleasure of chatting to Michael Kennedy, host of the <em>Talk Python</em> podcast (the chat was for an episode of our own show, <em>A Question of Code</em> - you should check it out). He's a charming and eloquent guy, and his show is well worth a listen. It highlights fascinating real-world applications of the Python programming language, lending a dash of context to a subject that can so often be presented in a clinical and abstract manner. <a href="https://talkpython.fm/episodes/show/270/python-in-supply-chains-oil-rigs-rockets-and-lettuce">Find it here</a>.</dd>
</dl>
<h2 id="not-a-podcast%2C-but..." tabindex="-1">Not a podcast, but...</h2>
<p>I got properly nerd-sniped this week by a video about painting restoration. Because I spend so much of my time working in front of a computer screen I'm always attracted to tales of creative and skillful "real world" craftsmanship - especially when they happen in a cool-looking workshop. Even if art conservation isn't your bag, stick with it until the presenter starts building a customised work table out of extruded aluminium. Hat-tip to my Dad for pointing me in the direction of this one. <a href="https://www.youtube.com/watch?v=OLxDD1xsjHw&feature=youtu.be">Watch it here</a>.</p>
<p>As always, feel free to forward this email to someone you think might like it. And if you've been forwarded this email (by someone awesome, no doubt), you can <a href="https://podcastsfornerds.com/">sign up for yourself at <em>podcastsfornerds.com</em></a> to be sent all future issues.</p>
<p>Thanks again,
Tom.</p>Late of this parish2020-07-22T00:00:00Zhttps://tomhazledine.com/podcasts-for-nerds/12-late-of-this-parish/<p><em>This week I've been thinking a lot about podcasts that are no longer with us. With the current boom in podcasting (are we at "peak podcast" yet? Maybe...) I keep seeing lots of faux-stats about the survival rate of most shows. Proportionally, very few make it past more that a couple of episodes, and hardly any make it past ten. The long tail is long, but not very consistent. There have been plenty of shows, however, that had really good runs and made quite an impact on me, but which have now ended (either officially killed-off or on indefinite hiatus).</em></p>
<h2 id="three-great-episodes-to-listen-to-this-week%3A" tabindex="-1">Three great episodes to listen to this week:</h2>
<dl>
<dt><a href="https://www.rocketjump.com/podcasts#tothefnfuture">Rob Auten (Video Game Writer)</a>, <em>To The F'ing Future</em> (122mins, 2013-04-17)</dt>
<dd>The epitome of American "Bros" talking nonsense, but <em>To The F'ing Future</em> contained some interesting discussions (if you can get past the host's personalities). They had a short run in 2013, talking about "the future" of a different topic each week. Episode #5 (with Rob Auten) is an interesting look at what the state-of-the-art in video-game plot and characterisation was like way back in 2013, and what they thought the future would hold. Which is even more interesting when viewed from 2020; we can see what played out and what sank without trace. If gaming isn't your thing, episode #6 was about tattooing (hear me out) and perhaps a little more "timeless". As with lots of podcasts I love, I have <em>no</em> interest in tattooing, but hearing an expert talk in depth about their work is exactly the kind of content I like the best. <a href="https://www.rocketjump.com/podcasts#tothefnfuture">Find it here</a>.</dd>
<dt><a href="https://www.earwolf.com/episode/renay-richardson-broccoli-content/">Renay Richardson, Broccoli Content</a>, <em>The Wolf Den</em> (50mins, 2019-01-31)</dt>
<dd>This podcast about the business of podcasting was very "inside baseball" but I loved it. Seemingly in the "indefinite hiatus" camp, this show interviewed some key figures in the behind-the-scenes world of podcasting, and always revealed new and interesting details about an industry that I find fascinating. <a href="https://www.earwolf.com/episode/renay-richardson-broccoli-content/">Find it here</a>.</dd>
<dt><a href="http://www.unfinished.bz/124">Art Directing the Web with Dan Mall</a>, <em>Unfinished Business</em> (65mins, 2018-04-18)</dt>
<dd>When I "pivoted" into my tech career back in 2013, podcasts helped me feel like I was connected to the industry. Hearing talented and enthusiastic designers and developers talk at length about their work was massively helpful, and this played right into the hands of my podcast addiction. <em>Unfinished Business</em> was a must-listen show for me, and taught be a lot about the web in general, and designing for the web specifically. This episode with returning-guest Dan Mall is from a later brief revival of the show (and therefore <em>slightly</em> more current), but there are some gems in the show's back catalogue. <a href="http://www.unfinished.bz/124">Find it here</a>.</dd>
</dl>
<h2 id="not-a-podcast%2C-but..." tabindex="-1">Not a podcast, but...</h2>
<p>Sadly in keeping with this week's theme of shows that are no longer running, I wanted to throw a shout-out to MPJ, the creator of the Fun Fun Function YouTube channel. He announced in an emotional video this month that the show is shutting down, and I'm really bummed out about it. MPJ's enthusiasm for programming was infectious, and I got a lot from his mission to teach fundamental skills in an entertaining way. If you like JavaScript and are thinking about getting into "functional" programming, then his original 2015 video series is a great place to start. <a href="https://www.youtube.com/watch?v=BMUiFMZr7vk&list=PL0zVEGEvSaeEd9hlmCXrk5yUyqUag-n84">Watch it here</a>.</p>
<p>As always, feel free to forward this email to someone you think might like it. And if you've been forwarded this email (by someone awesome, no doubt), you can <a href="https://podcastsfornerds.com/">sign up for yourself at <em>podcastsfornerds.com</em></a> to be sent all future issues.</p>
<p>Thanks again,
Tom.</p>Music. Music? Music!2020-07-15T00:00:00Zhttps://tomhazledine.com/podcasts-for-nerds/11-music-music-music/<p><em>I've got another loosely-themed issue for you. This week's podcast recommendations are all about music. That said, I've often received a pretty strong "that's not music" response every time I've tried to play tracks by The Books to friends and family. And as for bleepy-bloopy modular-synth based stuff, the disgust and repulsion was even stronger. So your milage may vary but I think all this stuff is bona-fide music, and I love it.</em></p>
<h2 id="three-great-episodes-to-listen-to-this-week%3A" tabindex="-1">Three great episodes to listen to this week:</h2>
<dl>
<dt><a href="https://songexploder.net/the-books">The Books: Smells Like Content</a>, <em>Song Exploder</em> (16mins, 2014-11-12)</dt>
<dd>If forced to compile a list of the Top Ten "Best" Podcasts, <em>Song Exploder</em> would certainly be on it. The show ticks all my boxes: a deep dive into a niche topic? Check. Beautifully produced? Check. An array of guests that aligns with my tastes? Check. An obsessive interest in picking apart the minutiae of a musical track, piece by piece? Check. (Okay, I'll admit there aren't too many shows that tick that last box). The show's creator, Hrishikesh Hirway, has proven on other shows to be a charming and sympathetic interviewer, but in <em>Song Exploder</em> he's worked hard to be invisible: he lets the musician and the music tell the story. This episode in particular is a great place to start because <em>Smells Like Content</em> is song that was written at the mixing desk: a collage of found-sounds and unusual production tricks. <a href="https://songexploder.net/the-books">Find it here</a>.</dd>
<dt><a href="https://www.whywebleep.com/whywebleep/2017/11/19/why-we-bleep-01-tom-whitwell-music-thing-modular">Tom Whitwell: Music Thing Modular</a>, <em>Why We Bleep</em> (81mins, 2018-01-21)</dt>
<dd>This newsletter is inadvertently charting my descent into an obsession with modular synthesis (kickstarted by the Not A Podcast from <a href="https://tomhazledine.com/podcasts-for-nerds/06-wet-loud-better/">issue #6</a>). The latest manifestation of my new interest is a willingness to hoover up any modular-related content, and having thankfully some of this comes in podcast-form. But if you don't share my interest in synth hardware, fear not: this discussion between performer and modular-YouTuber Mylar Melodies (real name: Alex) and module-creator/builder Tom Whitwel (real job: Digital Editor of The Times) covers wider and more interesting ground than just modular synthesis. Highlights include a section on "generative" music and some great anecdotes about encounters with the OG musical minimalist, La Monte Young. <a href="https://www.whywebleep.com/whywebleep/2017/11/19/why-we-bleep-01-tom-whitwell-music-thing-modular">Find it here</a>.</dd>
<dt><a href="https://podcasts.apple.com/gb/podcast/s1-ep2-maisie-peters/id1515347515?i=1000479001793">Maisie Peters</a>, <em>Ditty In A Dash</em> (22mins, 2020-06-22)</dt>
<dd>Despite considering myself to be (on some level, at least) a musician, traditional song-writing and lyric-creation has always been a blind spot of mine (as evidenced by my interest in bleepy-bloopy instrumental nonsense). This new podcast, <em>Ditty in a Dash</em> is a wholesome look behind the curtain at that very process. Host Frances challenges her music friends to create a song in fifteen minutes (or less) with just the barest bones of starting inspiration. The results are surprisingly effective, and the whole experience is thoroughly entertaining. Be warned, however: if the sound of young people being happy drives you crazy, this might not be a podcast for you. <a href="https://podcasts.apple.com/gb/podcast/s1-ep2-maisie-peters/id1515347515?i=1000479001793">Find it here</a>.</dd>
</dl>
<h2 id="not-a-podcast%2C-but..." tabindex="-1">Not a podcast, but...</h2>
<p>If the beeps and bloops and talented young people are all getting too much for you, you'll probably want to hit the wine. But what if you're not a traditional "wine snob", and find the lingo confusing? As always, YouTube's got your back. Video creator Sabrina Cruz neatly explains the problem, and recruits her editor Melissa Fernandes to help dig into the topic properly. The whole video is great, but be sure not to miss the excellent "flavour maps". This week's Not A Podcast was recommended by loyal newsletter reader Adam (do we need a label for us? "Nerds" feels like it's already taken. PFNers sounds a little too contrived. Suggestions on a postcard) <a href="https://www.youtube.com/watch?v=ELo8dfmfXr4&feature=youtu.be">Watch it here</a>.</p>
<p>As always, feel free to forward this email to someone you think might like it. And if you've been forwarded this email (by someone awesome, no doubt), you can <a href="https://podcastsfornerds.com/">sign up for yourself at <em>podcastsfornerds.com</em></a> to be sent all future issues.</p>
<p>Thanks again,
Tom.</p>Way more writing than you signed up for2020-07-08T00:00:00Zhttps://tomhazledine.com/podcasts-for-nerds/10-way-more-writing-than-you-signed-up-for/<p><em>This week heralds yet more evidence that podcasting is becoming a serious mainstream medium. Podcasting has been bubbling under the surface for years (decades?) as a niche nerdy pursuit, but following Spotify's $230 million acquisition of Gimlet Media (a pioneering podcast studio who are responsible for quite a few shows that have featured in this newsletter) early last year, the stakes have felt a little bit higher. Then in May Spotify dropped $100 million to bring the execrable Joe Rogan podcast into their fold (arguably making him <a href="https://www.theguardian.com/media/2020/may/24/spotify-podcast-deal-the-joe-rogan-experience">the highest paid broadcaster in the world</a>).</em></p>
<p><em>But now it's not just Spotify who are flashing their cash in the world of podcasting. US digital radio station SiriusXM bought the Stitcher podcasting studio this week <a href="https://www.wsj.com/articles/siriusxm-to-buy-stitcher-podcasting-unit-from-scripps-11594075192">for $300 million</a>. I don't think we've featured any Stitcher shows here before. My money was on "How did this get made?" being the first Stitcher show to be included here, but "Blowback" has beaten them to it (see below).</em></p>
<p><em>We're in a time where there's clearly more interest in podcasting from companies with deep pockets, and it cuts both ways. There's more funding and a bigger market for podcasters, but also the existential threat of platform lock-in (Spotify are big proponents of shows that only appear on their platform). As we've touched on before, is a podcast really a podcast if it doesn't have an RSS feed? Maybe... I'm still undecided.</em></p>
<h2 id="three-great-episodes-to-listen-to-this-week%3A" tabindex="-1">Three great episodes to listen to this week:</h2>
<dl>
<dt><a href="https://99percentinvisible.org/episode/their-dark-materials/">Their dark materials</a>, <em>99 Percent Invisible</em> (40mins, 2020-01-21)</dt>
<dd>Following on from last weeks' appearance of <em>Articles of Interest</em>, this week I'm including a "proper" episode of 99PI. The "feud" between artists Anish Kapoor and Stuart Semple is both hilarious and nerdy. Vantablack is (or was?) the world's "blackest black", but the carbon nanotube technology was exclusively licensed by Kapoor, thus prompting an inevitable backlash. Hot drama with the lowest of stakes; I love it. <a href="https://99percentinvisible.org/episode/their-dark-materials/">Find it here</a>.</dd>
<dt><a href="https://blowback.show/">Rosebud</a>, <em>Blowback</em> (58mins 2020-06-14)</dt>
<dd>I don't know how to classify this show. Is it a "two guys casually talking nonsense to themselves" deal? Yes. Is it a tightly produced and well researched history show? Also yes. Curious tone aside, this show is a detailed look at the events that lead to the 2003 invasion of Iraq. But it's also a little bit funny? As you can tell, I'm finding it all a bit confusing. But it's strangely compelling story telling, and I'll keep listening to the full series. <a href="https://blowback.show/">Find it here</a>.</dd>
<dt><a href="https://www.fullstackradio.com/episodes/142">Jason Cohen - Learning to Hire and Manage a Team</a>, <em>Full Stack Radio</em> (55mins 2020-07-01)</dt>
<dd><em>FSR</em> host Adam Wathan's 2-man business is doing okay. His book <em>Refactoring UI</em> has earned $2.2m since December 2018 (I own the book; it's great) and their "new" CSS UI library <em>Tailwind</em> has earned $1.8m since February this year. Now they're thinking about hiring someone, and Jason Cohen (the founder of massive hosting company WP Engine) is on hand to give him some much needed advice. <a href="https://www.fullstackradio.com/episodes/142">Find it here</a></dd>
</dl>
<h2 id="not-a-podcast%2C-but..." tabindex="-1">Not a podcast, but...</h2>
<p>Sometimes I want a deep dive into a densely technical topic, and other times all I want to do is watch ten years' worth of footage of the Sun spinning around. Thanks to NASA, I can do that now. <a href="https://www.youtube.com/watch?v=l3QQQu7QLoM">Watch it here</a>.</p>
<p>As always, feel free to forward this email to someone you think might like it. And if you've been forwarded this email (by someone awesome, no doubt), you can <a href="https://podcastsfornerds.com/">sign up for yourself at <em>podcastsfornerds.com</em></a> to be sent all future issues.</p>
<p>Thanks again,
Tom.</p>Dress smart, hear the echo, and ask great questions2020-07-01T00:00:00Zhttps://tomhazledine.com/podcasts-for-nerds/09-smart-echo-questions/<p><em>Favour time! I would love to be able to include some testimonials on the Podcasts for Nerds homepage. If you're enjoying these emails, it'd be great if you could tweet one thing you like about them; "I like this newsletter because it summarises podcasts I'm too lazy to listen to" - that sort of thing. Include the hashtag #podcastsForNerds, and I'll embed them on the PFN site.</em></p>
<h2 id="three-great-episodes-to-listen-to-this-week%3A" tabindex="-1">Three great episodes to listen to this week:</h2>
<dl>
<dt><a href="https://99percentinvisible.org/episode/suits-articles-of-interest-10/">Suits</a>, <em>Articles of Interest</em> (33mins, 2020-05-26)</dt>
<dd>Podcasting heavy-weight <em>99 Percent Invisible</em> will no doubt feature a lot in this newsletter; their deep-dives into niche topics are prime fodder for us nerds. For now, treat yourself to an episode from their clothing-focused mini-series, <em>Articles of Interest</em>. Avery Trufelman covers the history of the suit, from Beau Brummell through to the modern day. <a href="https://www.bbc.co.uk/programmes/m0002rmj">Find it here</a>.</dd>
<dt><a href="https://www.theworldaccordingtosound.org/2020/04/09/114-HagiaSophia.html">Sound Break: Hagia Sophia</a>, <em>The World According to Sound</em> (4mins, 2020-04-09)</dt>
<dd>This one sent shivers down my spine. In this remarkable four-minute show you get to hear some normal-sounding chanting, but then you get to hear it again as it would have sounded in the Hagia Sophia - a building with an 11 second (!) reverb. <a href="https://www.theworldaccordingtosound.org/2020/04/09/114-HagiaSophia.html">Find it here</a>.</dd>
<dt><a href="https://www.indiehackers.com/podcast/161-sam-parr-of-the-hustle">How to make millions by writing online (Sam Parr of the Hustle)</a>, <em>Indie Hackers</em> (60mins 2020-05-08)</dt>
<dd>People who have the self confidence to drag a profitable business out of literally nothing (like, the absolute tiniest shred of an idea) tend to <em>not</em> be the kind of people I'd want to hang out with, and I'm pretty sure Sam Parr is one of those guys. All of this interview is interesting on an academic level, but I find Parr himself quite annoying. However (and here's the real value-proposition of this newsletter) I've trawled through the whole thing to be able to tell you about the great section in the middle of this podcast where Parr outlines one of his key superpowers: asking questions. The kicker is that he then demonstrates it perfectly by grilling the host, Courtland Allen. <a href="https://www.indiehackers.com/podcast/161-sam-parr-of-the-hustle">Find it here</a>.</dd>
</dl>
<h2 id="not-a-podcast%2C-but..." tabindex="-1">Not a podcast, but...</h2>
<p>Tom Scott's "Things you might not know" series has been running for years, and all of them are great (if fact, his whole channel is consistently awesome in the most nerdy way possible). His latest, <em>Why You Can Spot Bad Green Screen</em> is particularly relevant to those of us who've been messing about with Zoom's "virtual backgrounds" lately (as well as simply highlighting why some bad TV looks so bad). <a href="https://www.youtube.com/watch?v=E5HRvQNg4pQ">Watch it here</a>.</p>
<p>As always, feel free to forward this email to someone you think might like it. And if you've been forwarded this email (by someone awesome, no doubt), you can <a href="https://podcastsfornerds.com/">sign up for yourself at <em>podcastsfornerds.com</em></a> to be sent all future issues. And I'm serious about the Twitter thing; it'd really help me out.</p>
<p>Thanks again,
Tom.</p>Chaos, productivity, and more chaos2020-06-24T00:00:00Zhttps://tomhazledine.com/podcasts-for-nerds/08-chaos-productivity-more-chaos/<p><em>We often hear that "discovery" is the biggest unsolved problem in podcasting. I'm trying to address that problem with this newsletter, and the very fact that you've subscribed means you're actively seeking out new podcasts to listen to. But beyond knowing what <em>I</em> do, I don't really know how people find out about shows. So tell me. Reply to this email and let me know the most common way you hear about new shows. I'll share the best tactics next week (spoiler alert: tactic #1 is "subscribe to Podcasts for Nerds", so be sure to tell your friends).</em></p>
<h2 id="three-great-episodes-to-listen-to-this-week%3A" tabindex="-1">Three great episodes to listen to this week:</h2>
<dl>
<dt><a href="http://weare.netflix.net/">Chaos Engineering at Netflix</a>, <em>WeAreNetflix</em> (51mins, 2019-02-04)</dt>
<dd>I find the idea of "chaos monkeys" captivating. A chaos monkey is an automated program that will turn off pieces of a company's key infrastructure without warning; essentially the digital equivalent of running through a data centre and unplugging random cables. In theory it's a great way to test the durability of an online system, and the idea has been championed by the engineering team at Netflix (one of the biggest instances of online infrastructure in the world). I'm not certain who this podcast is <em>for</em> - I suspect it's mostly a recruiting exercise aimed at attracting engineers - but it's very interesting nonetheless. <a href="http://weare.netflix.net/">Find it here</a>.</dd>
<dt><a href="https://www.relay.fm/cortex/101">Productivity 101</a>, <em>Cortex</em> (110mins, 2020-05-11)</dt>
<dd>There are a handful of shows where I <em>never</em> miss an episode, but they're hard to recommend to people because the appeal of the show comes from having gotten to "know" the hosts after listening for ages (years, in some cases). <em>Cortex</em> is one of those; beyond "start at the beginning and see if you like it", it's tough to come up with definitive episodes for people to try out. Thankfully a recent instalment has (sort-of) addressed this by handily distilling all their productivity tips into a single episode. (And if you like this one, my best advice is to go back to their first episode and follow along from there.) <a href="https://www.relay.fm/cortex/101">Find it here</a>.</dd>
<dt><a href="https://www.npr.org/podcasts/674580962/the-big-one-your-survival-guide">The Earthquake</a>, <em>The Big One</em> (30mins, 2019-01-10)</dt>
<dd>If you got a kick out of the real-time-disaster-reporting of <em>Floodlines</em> (recommended in <a href="https://tomhazledine.com/podcasts-for-nerds/06-wet-loud-better/">issue #6</a>), then you'll love <em>The Big One</em>. Thankfully the premise of this show is speculative (for now). The city of LA sits on a massive geological fault line, and <em>will</em> be hit by a massive and potentially devastating earthquake. But no-one can say when, so preparations are maybe not as thorough as they should be. This excellently-produced show charts what would happen when the <em>The Big One</em> finally hits.</dd>
</dl>
<h2 id="not-a-podcast%2C-but..." tabindex="-1">Not a podcast, but...</h2>
<p>I'm guessing you've all seen the videos of Spot; simultaneously the cutest and the creepiest robot around. Well now Spot's creators, Boston Dynamics (odds-on favourites for instigating the inevitable robot uprising), are selling Spots to anyone with a <a href="https://www.theverge.com/21292684/boston-dynamics-spot-robot-on-sale-price">spare $74,500</a> lying around. The most engaging "comercial" application so far is seeing Spot trialed as a sheep dog in New Zealand. <a href="https://www.youtube.com/watch?v=gD7K6-q-o50">Watch the madness here</a>.</p>
<p>As always, feel free to forward this email to someone you think might like it. And if you've been forwarded this email (by someone awesome, no doubt), you can <a href="https://podcastsfornerds.com/">sign up for yourself at <em>podcastsfornerds.com</em></a> to be sent all future issues.</p>
<p>Thanks again,
Tom.</p>Maximal, minimal, optimal2020-06-17T00:00:00Zhttps://tomhazledine.com/podcasts-for-nerds/07-maximal-minimal-optimal/<p><em>Most of the recommendations in this newsletter have been at or around the thirty-minutes-long mark. Many of you have commented that this is "just right" and probably "the best length of podcast to be recommending". So naturally I'm mixing it up for this issue. There's a long rambling chat about supply chains (my personal favourite podcast format!), and a short snappy tidbit that's been really well produced. Plus a show at the "optimal" length. But you tell me: reply to this email and let me know what <strong>your</strong> favourite length is for a podcast.</em></p>
<h2 id="three-great-episodes-to-listen-to-this-week%3A" tabindex="-1">Three great episodes to listen to this week:</h2>
<dl>
<dt><a href="https://www.sourceresshq.com/tea">In pursuit of tea</a>, <em>Sourceress</em> (52mins, 2019-09-04)</dt>
<dd><em>Sourceress</em> is a podcast about supply chains, and this episode is an interview with a New York based tea importer. It's a long chat conducted during a tea ceremony, but stick with it because there's some fascinating stuff in here. Learn all about how the picking process determines the type of tea, and also why you can’t sell Americans tea that tastes “shrimpy”. <a href="https://www.sourceresshq.com/tea">Find it here</a>.</dd>
<dt><a href="https://www.npr.org/podcasts/813012842/science-diction?t=1592370974422">Dinosaur</a>, <em>Science Diction</em> (12mins, 2020-03-10)</dt>
<dd>Richard Owen won acclaim as the world's first palaeontologist and inventor of the word "dinosaur". Trouble is, Owen himself was the one doing the acclaiming. With a talent for naturalism, and an even bigger talent for self promotion, Owen was a controversial figure who picked a fight with one of the world's most revered scientists. <a href="https://www.npr.org/podcasts/813012842/science-diction?t=1592370974422">Find it here</a>.</dd>
<dt><a href="https://headgum.com/dead-eyes/1-hes-having-second-thoughts">He's having second thoughts</a>, <em>Dead Eyes</em> (33mins, 2020-01-23)</dt>
<dd>What do you do when you've been fired by the most likeable man in the world? Actor and comedian Connor Ratliff's career was almost over before it had begun, and twenty years later he's digging into what happened when Tom Hanks decided he had "dead eyes". This is an odd story, hilariously told. <a href="https://headgum.com/dead-eyes/1-hes-having-second-thoughts">Find it here</a>.</dd>
</dl>
<h2 id="not-a-podcast%2C-but..." tabindex="-1">Not a podcast, but...</h2>
<p>There aren't many of my fellow nerds that are as interested in late-nineties BMWs as I am, but most nerds I know <em>are</em> interested in cameras and the craft of filmmaking. This film is a quick look at the concept of the "chase car": a fast but practical car that has a monstrous camera rig strapped to it and is used to get all the fantastic shots of supercars travelling at speed. Even if you're not as excited by an E39 M5 as I am, you've got to admit the footage is stunning. <a href="https://www.youtube.com/watch?v=wXr3d2-wAOM">Find it here</a>.</p>
<p>As always, feel free to forward this email to someone you think might like it. And if you've been forwarded this email (by someone awesome, no doubt), you can <a href="https://podcastsfornerds.com/">sign up for yourself at <em>podcastsfornerds.com</em></a> to be sent all future issues.</p>
<p>Thanks again,
Tom.</p>Getting wet, getting loud, getting better2020-06-10T00:00:00Zhttps://tomhazledine.com/podcasts-for-nerds/06-wet-loud-better/<p><em>At the start of the lockdown I asked my Twitter followers how their podcast listening habits had changed. The majority of responders said they were listening to fewer podcasts during the "new normal". A lot of people use their commute as podcast-time! Personally I've replaced the ~10 hours a week travel-time with ~10 hours a week staring-at-a-blank-wall-musing-on-the-futility-of-existence time, so I'm actually listing the same volume of podcasts as before. How have your listening habits changed over the past months? Reply to this email and tell me.</em></p>
<h2 id="three-great-episodes-to-listen-to-this-week%3A" tabindex="-1">Three great episodes to listen to this week:</h2>
<dl>
<dt><a href="https://www.theatlantic.com/podcasts/floodlines/">Antediluvian</a>, <em>Floodlines</em> (32mins, 2020-03-12)</dt>
<dd>Vann R. Newkirk II is quite a name! <em>Floodlines</em> is his beautifully told examination of the "unnatural disaster" that occurred when the coasts of Louisiana and Mississippi were hit by a Category 5 tropical cyclone in August, 2005. This is a story about Hurricane Katrina, but it's not all about the meteorology... <a href="https://www.theatlantic.com/podcasts/floodlines/">Find it here</a>.</dd>
<dt><a href="https://gimletmedia.com/shows/mogul/wbhjzr/part-1-that-beat-that-beat-right-there">That beat, that beat right there</a>, <em>Mogul</em> (38mins, 2017-07-16)</dt>
<dd>The story of music-manager Chris Lighty is tied to the story of hip-hop, and <em>Mogul</em> tells that story. Lighty was a rough-and-tumble entrepreneur, and his sorry tale makes for strangely compelling listening. The series charts his rise and inevitable fall, so I'd recommend starting at episode one to get the full arc. <a href="https://gimletmedia.com/shows/mogul/wbhjzr/part-1-that-beat-that-beat-right-there">Find it here</a>.</dd>
<dt><a href="https://www.aboutracepodcast.com/1-things-can-only-get-better">Things can only get better</a>, <em>About Race</em> (25mins, 2018-03-22)</dt>
<dd>Well produced and well thought out, this podcast was an instant hit when it was released in 2018 and has been hovering around the top of the charts again this week. In episode #1, Host Reni Eddo-Lodge (author of <em>Why I’m no longer talking to white people about race</em>) looks at the golden era of multiculturalism in 90's Britain, and asks what went wrong. <a href="https://www.aboutracepodcast.com/1-things-can-only-get-better">Find it here</a>.</dd>
</dl>
<h2 id="not-a-podcast%2C-but..." tabindex="-1">Not a podcast, but...</h2>
<p>If you like memes, you've probably seen Andrew Huang's work (if not, be sure to check out his "99 red balloons played with red balloons" video). But as well as making viral music clips he also shares his immense music production knowledge. His studio is epic, so if you're interested in synths and music making in general, you'll love his <em>Modular synthesis explained</em> video. <a href="https://youtu.be/cWslSTTkiFU">Find it here</a>.</p>
<p>Thanks for reading. If you've enjoyed this week's issue of Podcasts for Nerds, feel free to forward it to a friend or two. I'm in "growth hacking" mode now, so I'll be extra grateful for any bonus promotion you can give me. And if you've been forwarded this email (by someone awesome, no doubt), you can <a href="https://podcastsfornerds.com/">sign up for yourself at <em>podcastsfornerds.com</em></a> to be sent all future issues.</p>
<p>Thanks again,
Tom.</p>Podcast or not? You decide2020-06-03T00:00:00Zhttps://tomhazledine.com/podcasts-for-nerds/05-podcast-or-not/<p><em>The boundary between podcasts and radio shows is blurry. If the BBC release one of their shows online, does it become a podcast? Objectively, yes; if it has an RSS feed of audio files then it <strong>is</strong> a podcast. But it's not really what I think of when I think of "podcasting". The constraints of Old Media are still there, whereas the main appeal of podcasts (in my view) is the lack of constraints. They can be as long as the creators feel they need to be (ideally letting the content dictate the length), and they can go deeper into topics than any show aimed at a "general" audience could ever dare to go. Conversely, they can be <strong>less</strong> focused and more rambling and more amateurish than any show on the radio. If someone somewhere finds it interesting or entertaining, then it is a valid podcast.</em></p>
<p><em>With that in mind, this week includes two BBC shows that <strong>do</strong> pass my internal "podcasting duck-test" (If it looks like a podcast and quacks like a podcast, it's probably safe to call it a podcast). And to even things out, the final recommendation is a show that could <strong>only</strong> exist as a podcast.</em></p>
<h2 id="three-great-podcast-episodes-i've-listened-to-this-week%3A" tabindex="-1">Three great podcast episodes I've listened to this week:</h2>
<ol>
<li>
<p><a href="https://www.bbc.co.uk/programmes/m000hw0b">Jon Ronson</a>, <em>Grounded with Louis Theroux</em> (58mins, 2020-05-09)
Louis Theroux can't make documentaries because of the lockdown, so he's been having conversations with his famous friends instead. In this episode, Louis and fellow documentarian Jon Ronson compare notes on their surprisingly similar careers. Lighthearted and entertaining, this episode's highlight is the surprisingly nuanced discussion of "de-platforming" controversial figures like Alex Jones and David Icke. <a href="https://www.bbc.co.uk/programmes/m000hw0b">Find it here</a>.</p>
</li>
<li>
<p><a href="https://www.bbc.co.uk/programmes/w3csz4dn">The fourth astronaut</a>, <em>13 minutes to the moon</em> (48mins, 2019-06-12)
The second season of 13MTTM has just finished. It told the story of Apollo 13, and is a highly-recommended listen. If you've not heard about this podcast before, however, you'll be best-off starting with series 1, which is all about Apollo 11's final 13-minute descent to the surface of the moon. The show focusses on the team behind the missions, and includes remarkable insights into the specifics of every tiny moment in the flight recordings of that historic descent. For nerds like me, this episode about the onboard computer is particularly interesting. 13MTTM has just about the highest production value of any podcast I've heard, no doubt mostly owing to the original score by Hans Zimmer. <a href="https://www.bbc.co.uk/programmes/w3csz4dn">Find it here</a>.</p>
</li>
<li>
<p><a href="https://oxide.computer/podcast/on-the-metal-6-kenneth-finnegan/">Kenneth Finnegan</a>, <em>On the metal</em> (65mins, 2020-01-06)
As the show's name suggests, this podcast goes deep into the weeds of CoLo servers, ISPs, and computer networking. Kenneth's tale has elements of Forrest Gump in the sheer good luck and fortuitousness of his decisions (but with none of FG's sadness). Regardless of how much you know about the tech in question, hearing how all the pieces fell together makes for a great story. <a href="https://oxide.computer/podcast/on-the-metal-6-kenneth-finnegan/">Find it here</a>.</p>
</li>
</ol>
<h2 id="not-a-podcast%2C-but..." tabindex="-1">Not a podcast, but...</h2>
<p>It feels a bit odd to continue writing about tech and podcasts when the world is on fire. I think it's important to show our support for those in peril in the US, but also to not talk over the voices of people who actually know what they're talking about. <a href="https://blacklivesmatters.carrd.co">Donate if you can #BLM</a></p>
<p>Stay safe out there (or <em>in</em> there, if you're still locked down).
Tom.</p>Cracking, hacking, and phishing2020-05-27T00:00:00Zhttps://tomhazledine.com/podcasts-for-nerds/04-cracking-hacking-phishing/<p><em>"Hacker" is a word with several meanings. In the vernacular, it's taken to mean a tech-savvy bad-actor; "a hacker broke into the bank's database", "the NSA mainframe was hacked", etc. But in the start-up sphere, it just means someone who is creative with technology; "she hacked together her MVP in the garage", "did you see that fantastic hack she made to fix that bug?". This week's issue of <a href="https://www.podcastsfornerds.com/">Podcasts for Nerds</a> contains both types.</em></p>
<h2 id="3-great-podcast-episodes-i've-listened-to-this-week%3A" tabindex="-1">3 great podcast episodes I've listened to this week:</h2>
<dl>
<dt><a href="http://shoptalkshow.com/episodes/special-one-one-hacker/">One on One with a Hacker</a>, <em>Shoptalk Show</em> (56mins, 2014-03-24)</dt>
<dd>After having had his website "hacked", Shoptalk host Chris Coyier was able to actually interview the person who hacked the site. Seriously, folks; be on your guard for Behavioural Engineering. <a href="http://shoptalkshow.com/episodes/special-one-one-hacker/">Find it here.</a></dd>
<dt><a href="http://creativecodingpodcast.com/hacking-classic-nintendo-guns-and-going-viral/">Hacking classic Nintendo guns and going viral</a>, <em>The Creative Coding Podcast</em> (46mins, 2016-09-07)</dt>
<dd>Seb Lee-Delisle is a digital marker/artist/inventor who's laser events are the stuff of legend. In this episode of Creative Coding (RIP), he does a deep-dive on how he reverse-engineered a Nintendo light-gun for his own creative ends. <a href="http://creativecodingpodcast.com/hacking-classic-nintendo-guns-and-going-viral/">Find it here</a></dd>
<dt><a href="https://gimletmedia.com/shows/reply-all/rnhoww/97-what-kind-of-idiot-gets-phished">What kind of idiot gets phished?</a> <em>Reply All</em> (30mins, 2017-05-18)</dt>
<dd>In a break from the show's usual format, one of Reply All's producers takes over and attempts to "phish" her colleagues (and bosses!). A eye-opening look at how subtle some attackers can be; even if you're already on the look out! <a href="https://gimletmedia.com/shows/reply-all/rnhoww/97-what-kind-of-idiot-gets-phished">Find it here</a></dd>
</dl>
<h2 id="not-a-podcast%2C-but..." tabindex="-1">Not a podcast, but...</h2>
<p>Jay Forman's eccentric look at the history of London's boroughs is full of great pub-quiz level trivia and also manages to be hilarious (if you like that sort of thing). The two-part series starts with the 11min YouTube video "Why does London have 32 boroughs?", and I recommend you watch them both. <a href="https://www.youtube.com/watch?v=daeB46Z4fjs">Find them here</a></p>
<p>Thanks again for reading. I'm investigating different ways to manage this list, which is why this issue looks a little more "fancy" than last week's. What do you think? Do you like the new look, or did you prefer the plain-text version?</p>
<p>Cheers,
Tom.</p>Soundscapes, a grounding, and a year in isolation2020-05-20T00:00:00Zhttps://tomhazledine.com/podcasts-for-nerds/03-soundscapes-grounding-isolation/<p><em>Thanks for all the great feedback on the format of this newsletter! Keep it coming; it's really helpful. I promised myself I'd only make a website for this site if we could reach certain number of readers in the first two weeks. Thanks to you we've passed that mark, so now this list has a name ("Podcasts for Nerds") and a (placeholder) website: <a href="https://www.podcastsfornerds.com/">podcastsfornerds.com</a>.</em></p>
<h2 id="3-great-podcast-episodes-i've-listened-to-this-week%3A" tabindex="-1">3 great podcast episodes I've listened to this week:</h2>
<dl>
<dt><a href="https://www.20k.org/episodes/pewpew">Pew Pew</a>, <em>Twenty Thousand Hertz</em> (38mins, 2020-05-13)</dt>
<dd>This sound-design podcast is full of gems, and we'll no doubt be returning to it in future editions of Podcasts for Nerds. The most recent episode looks at the work of legnedary sound designer Ben Burtt, who created the soundscapes for Star Wars. A real highlight is when the 20kHtz team deconstruct and recreate some sounds that will be familiar to all of us. Who knew that elphant growls were so terrifying?! <a href="https://www.20k.org/episodes/pewpew">Find it here</a></dd>
<dt><a href="http://timharford.com/2019/11/cautionary-tales-ep-1-danger-rocks-ahead/">Danger: rocks ahead!</a>, <em>Cautionary Tales</em> (36mins, 2019-11-15)</dt>
<dd>Plan Continuation Bias (also known by pilots as "get-there-itis") can have catastrophic effects. Economist Tim Harford takes a look at what we can learn from disasters, starting with the sorry tale of the Torrey Canyon. <a href="http://timharford.com/2019/11/cautionary-tales-ep-1-danger-rocks-ahead/">Find it here</a></dd>
<dt><a href="https://gimletmedia.com/shows/the-habitat/awhjjr/episode-1-this-is-the-way-up">This is the way up</a>, <em>The Habitat</em> (25mins, 2018-04-18)</dt>
<dd>There's a lot of stuff that needs to be thoroughly tested before we can go to Mars, but the biggest unknown is not what equipment we'll take or what propulsion system we'll use. The most important thing to test is, well, us! The Habitat documentsone of NASA's year-long HI-SEAS experiments to see how a team of humans cope with being isolated for a full year. For science! <a href="https://gimletmedia.com/shows/the-habitat/awhjjr/episode-1-this-is-the-way-up">Find it here</a></dd>
</dl>
<h2 id="not-a-podcast%2C-but..." tabindex="-1">Not a podcast, but...</h2>
<p>If <em>useful</em> maths is something that appeals, then 3Blue1Brown is a YouTube channel for you. With the help of some elegant animation, the recent "Simulating an epidemic" video digs into the theories behind different lockdown strategies. Eye-opening and informative, but still remains fact-based and theoretical. <a href="https://www.youtube.com/watch?v=gxAaO2rsdIs">Find it here</a></p>
<p>Thanks again for being on this list. Some of you are already recommending episodes to me (thanks, Mike!), and that is strongly encouraged! What episode do <em>you</em> think should appear in a future edition of Podcasts for Nerds?
Cheers,
Tom.</p>Podcasts for Nerds2020-05-14T00:00:00Zhttps://tomhazledine.com/podcasts-for-nerds/<script src="https://f.convertkit.com/ckjs/ck.5.js"/><div id="newsletter-signup" class="intro-note stack--small"><h3>Signup to my newsletter</h3><p>Join the dozens (dozens!) of people who get my writing delivered directly to their inbox. You'll also hear news about my miscellaneous other projects, some of which never get mentioned on this site.</p><form action="https://app.convertkit.com/forms/3039830/subscriptions" class="seva-form formkit-form" method="post" data-sv-form="3039830" data-uid="b7d64123d3" data-format="inline" data-version="5" data-options="{"settings":{"after_subscribe":{"action":"redirect","success_message":"Success! Now check your email to confirm your subscription.","redirect_url":"https://tomhazledine.com/newsletter/pending"},"analytics":{"google":null,"facebook":null,"segment":null,"pinterest":null,"sparkloop":null,"googletagmanager":null},"modal":{"trigger":"timer","scroll_percentage":null,"timer":5,"devices":"all","show_once_every":15},"powered_by":{"show":true,"url":"https://convertkit.com/features/forms?utm_campaign=poweredby&utm_content=form&utm_medium=referral&utm_source=dynamic"},"recaptcha":{"enabled":false},"return_visitor":{"action":"show","custom_content":""},"slide_in":{"display_in":"bottom_right","trigger":"timer","scroll_percentage":null,"timer":5,"devices":"all","show_once_every":15},"sticky_bar":{"display_in":"top","trigger":"timer","scroll_percentage":null,"timer":5,"devices":"all","show_once_every":15}},"version":"5"}" min-width="400 500 600 700 800"><div data-style="clean"><ul class="formkit-alert formkit-alert-error" data-element="errors" data-group="alert"/><div data-element="fields" data-stacked="false" class="intro-note__form seva-fields formkit-fields"><div class="intro-note__form-field formkit-field"><input class="formkit-input" aria-label="What should I call you?" name="fields[first_name]" placeholder="What should I call you?" type="text"/></div><div class="intro-note__form-field formkit-field"><input class="formkit-input" name="email_address" aria-label="What's your email address?" placeholder="What's your email address?" type="email"/></div><button data-element="submit" class="button formkit-submit"><div class="formkit-spinner"><div/><div/><div/></div><span class="">Subscribe</span></button></div></div></form></div><h3 id="what-is-podcast-for-nerds">What is Podcast for Nerds?</h3><ul><li>💌One email with three recommended podcast episodes, sent every Wednesday.</li><li>🎙️Each recommendation has a short summary explaining why you might like it.</li><li>👀Read <strong>issue #12</strong> right now: <a href="https://tomhazledine.com/podcasts-for-nerds/11-late-of-this-parish/"><em>Late of this parish</em></a></li><li>📮<a href="#newsletter-signup">Subscribe</a> to read <strong>issue #15</strong></li></ul><blockquote><p>Issues are archived online after a few weeks, but the only way to get them "fresh" is to sign up to the newsletter. If you subscribe you'll get to read the most recent issue straight away, and all future issues will wing their way directly to your inbox.</p></blockquote><h3 id="why-am-i-writing-this-newsletter">Why am I writing this newsletter?</h3><p>I'm probably overly obsessed by podcasts. I'm currently subscribed to way more shows than anyone else I know, with even more individual episodes queued-up for review. Every (sensible) person I know listens to a few, but I definitely listen to too many.</p><p>So now I'm trying to turn that bug into a feature.</p><p>I can save you time by summarising the best episodes I've listened to lately. There's loads of fantastic stuff out there, but you have to listen to a lot of average episodes to be sure of hearing the good bits. Let me do that digging for you.</p><h2 id="recent-issues">Recent issues:</h2><ul><li><strong>#15.</strong> <em>Quirks mode</em> (<a href="#newsletter-signup">sign up now to read this</a>)</li><li><strong>#14.</strong> <em>Riding the modular wave</em></li><li><strong>#13.</strong> <em>Lots of things. No theme.</em></li><li><strong>#12.</strong> <a href="https://tomhazledine.com/podcasts-for-nerds/12-late-of-this-parish/"><em>Late of this parish</em></a></li><li><strong>#11.</strong> <a href="https://tomhazledine.com/podcasts-for-nerds/11-music-music-music/"><em>Music. Music? Music!</em></a></li><li><strong>#10.</strong> <a href="https://tomhazledine.com/podcasts-for-nerds/10-way-more-writing-than-you-signed-up-for/"><em>Way more writing than you signed up for</em></a></li><li><strong>#9.</strong> <a href="https://tomhazledine.com/podcasts-for-nerds/09-smart-echo-questions/"><em>Dress smart, hear the echo, and ask great questions</em></a></li><li><strong>#8.</strong> <a href="https://tomhazledine.com/podcasts-for-nerds/08-chaos-productivity-more-chaos/"><em>Chaos, productivity, and more chaos</em></a></li><li><strong>#7.</strong> <a href="https://tomhazledine.com/podcasts-for-nerds/07-maximal-minimal-optimal/"><em>Maximal, minimal, optimal</em></a></li><li><strong>#6.</strong> <a href="https://tomhazledine.com/podcasts-for-nerds/06-wet-loud-better/"><em>Getting wet, getting loud, getting better</em></a></li><li><strong>#5.</strong> <a href="https://tomhazledine.com/podcasts-for-nerds/05-podcast-or-not/"><em>Podcast or not? You decide</em></a></li><li><strong>#4.</strong> <a href="https://tomhazledine.com/podcasts-for-nerds/04-cracking-hacking-phishing/"><em>Cracking, hacking, and phishing</em></a></li><li><strong>#3.</strong> <a href="https://tomhazledine.com/podcasts-for-nerds/03-soundscapes-grounding-isolation/"><em>Soundscapes, a grounding, and a year in isolation</em></a></li><li><strong>#2.</strong> <a href="https://tomhazledine.com/podcasts-for-nerds/02-tannoys-pitches-traction/"><em>Tannoys, tragic pitches, and business traction</em></a></li><li><strong>#1.</strong> <a href="https://tomhazledine.com/podcasts-for-nerds/01-prescience-beginnings-linguistics/"><em>Prescience, new beginnings, and modern linguistics</em></a></li></ul><p><a href="#newsletter-signup">Sign up</a> to read the latest issue (and get a new one straight into you inbox every Wednesday).</p>Tannoys, Tragic Pitches, and Business Traction2020-05-13T00:00:00Zhttps://tomhazledine.com/podcasts-for-nerds/02-tannoys-pitches-traction/<dl>
<dt><a href="https://overcast.fm/+ZsHO6amM8">The Final Destination</a>, <em>Tales from the Tannoy</em> (32mins, 2020-04-21)</dt>
<dd>We probably hear the work of voice-over artists every day, but don't even notice them. <em>Tales from the Tannoy</em> is presented by Elinor Hamilton, the voice of the London Underground. The show interviews people whose voices we probably know but whose lives we know nothing about. This episode features Jon Briggs, the original voice of Apple's Siri and the voice inside many sat navs. He's charming and (obviously) has a fantastic voice, and his story takes a rather unexpected turn. <a href="https://overcast.fm/+ZsHO6amM8">Find it here</a>.</dd>
<dt><a href="https://gimletmedia.com/shows/startup/6nh3zg/gimlet-1-how-not-to-pitch-a-billionaire">How not to pitch a billionaire</a>, <em>Startup</em> (25mins, 2014-04-05)</dt>
<dd>Host Alex Blumberg had a vision for what a podcast company could be, and documented his journey to get there. The first episode, above all others, is podcasting at its best. The part where Alex messes up a pitch to infamous VC Chris Sacca is a must-listen. Even if you're not into tech or business stories, this is an episode I think you should listen to: IMO this marked the point at which podcasting started to really take itself seriously. <a href="https://gimletmedia.com/shows/startup/6nh3zg/gimlet-1-how-not-to-pitch-a-billionaire">Find it here</a>.</dd>
<dt><a href="https://overcast.fm/+Lr-kJYCUA">A publishing platform built for independent writers</a>, <em>The Business of Content</em> (43mins, 2020-04-13)</dt>
<dd>I've heard John O'Nolan tell the story of how he founded his publishing platform before. How he started Ghost is an object lesson in how to capitalise on a flash of viral popularity (step 1: be an expert in a field and come up with an idea for something everybody wants). In addition to the fantastic origin story, this interview addresses the problems many news organisations are having (still!) getting readers to pay for content online. <a href="https://overcast.fm/+Lr-kJYCUA">Find it here</a>.</dd>
</dl>
<h2 id="not-a-podcast%2C-but..." tabindex="-1">Not a podcast, but...</h2>
<p>I didn't even know people out there were adding salt to their coffee, but apparently it is "a thing". James Hoffmann has done some testing in his video on The Magic of Salt in Coffee. Things get really entertaining when the Nescafé makes an appearance... James' YouTube channel is full of coffee-related nerdery, and is definitely worth exploring. <a href="https://www.youtube.com/watch?v=9PUWQQ-joKE">Find it here</a>.</p>
<p>Happy listening!</p>
<p>Tom.</p>Spiraling out of control? Open up the Bullet Journal again2020-05-06T00:00:00Zhttps://tomhazledine.com/bullet-journal-redux/<p>Believe it or not, I've thought of something new to say about Bullet Journalling. I won't dive into the mechanics of <em>what</em> and <em>how</em>, because I've written about <a href="https://tomhazledine.com/bullet-journal-revisited">Bullet Journalling before</a> (<a href="https://tomhazledine.com/bullet-journal-workflow">several times</a>, in fact). If you're unfamiliar with the concept of Bullet Journalling, those posts are better places to start.</p>
<p>What I've noticed lately is the <em>pattern</em> behind my Bullet Journal usage. I've picked up the journalling habit several times now, and dropped it just as many times too.</p>
<p>My journal was a constant presence when I was freelancing, but it fell by the wayside when I became an employee at a company. I picked it up again when I needed to find a new job, and it was a massively useful tool in that process. But once gainfully employed again, the journal soon began to gather dust. And when a company I was working at shut down suddenly, out came the journal again and the same pattern repeated.</p>
<p>I've started using my Bullet Journal again recently, too. But this time I'm not in need of a new job, and I'm not freelancing. This time the entire world is on fire and falling apart around us<sup class="footnote-ref"><a href="#fn1" id="fnref1">[1]</a></sup>. And that's the pattern; when things feel like they're slipping beyond my control, Bullet Journalling helps me stay on course and stay (or get back to being) productive.</p>
<p>What I didn't realise before is that journalling is a transactional process for me. It's not the life-long everyday habit that I first thought it would be. It's a useful mechanism to help deal with times of crisis. Pick it up, reap the benefits for a few weeks, and then discard it again.</p>
<p>When I <em>do</em> maintain a Bullet Journal, I'm pretty thorough and update it every morning. But I'm still using the first notebook I bought for the job (from a pack of three I picked up in 2013!). Flicking through the pages, I can see a written record of all my periods of uncertainty, transition, and crisis<sup class="footnote-ref"><a href="#fn2" id="fnref2">[2]</a></sup>.</p>
<p>I'm not saying that <em>everyone</em> should take up journalling when their lives start slipping, but this is certainly an effective coping mechanism for <em>me</em>. And being aware of journaling's function in my life is only going to make the tool more useful. I'll no longer feel guilty when I inevitably stop maintaining the journal - it's not a chore that "must be done", its a tool that has done its job. And maybe I'll adopt the journaling habit sooner next time; maybe get things back on-track <em>before</em> things bubble over into a productivity crisis.</p>
<hr class="footnotes-sep"/>
<section class="footnotes">
<ol class="footnotes-list">
<li id="fn1" class="footnote-item"><p>I'm hoping some of you are reading this from a time that's not gripped by a global pandemic. <em>Sic transit gloria</em> and all that. <a href="#fnref1" class="footnote-backref">↩︎</a></p>
</li>
<li id="fn2" class="footnote-item"><p>Made much easier by the fact that I'm maintaining a detailed "contents" page, like the nerd that I am. <a href="#fnref2" class="footnote-backref">↩︎</a></p>
</li>
</ol>
</section>Prescience, new beginnings, and modern linguistics2020-05-06T00:00:00Zhttps://tomhazledine.com/podcasts-for-nerds/01-prescience-beginnings-linguistics/<!-- ## 3 great episodes I've listened to this week: -->
<dl>
<dt><a href="https://www.numberphile.com/podcast/hannah-fry-coronavirus">Crystal balls and Coronavirus</a>, <em>The Numberphile Podcast</em> (45mins, 2020-04-10)</dt>
<dd>Numberphile is a YouTube channel for maths-nerds, and the spin-off podcast interviews leading mathematicians. In this episode TV-mathematician Hannah Fry looks back on the lessons learned (or not learned) from the 2018 BBC show <em>Contagion</em> (which she presented). That show was a remarkably prescient look at what a modern pandemic would look like. <a href="https://www.numberphile.com/podcast/hannah-fry-coronavirus">Find it here</a></dd>
<dt><a href="https://www.relay.fm/b-sides/41">The idea</a>, <em>Backstage</em> (28mins, 2020-04-27)</dt>
<dd>Myke and Stephen run Relay FM, a podcast network that produces loads of good shows I listen to all the time. They're starting a new show that's a "behind the scenes" look at creating a podcast (meta, I know!). The whole show will be behind a members-only paywall, but the first episode is free for all and really interesting. <a href="https://www.relay.fm/b-sides/41">Find it here</a></dd>
<dt><a href="https://soundcloud.com/lingthusiasm/43-the-grammar-of-singular-they-interview-with-kirby-conrod">The grammar of singular they</a>, <em>Lingthusiasm</em> (42mins, 2020-04-17)</dt>
<dd>Everyone loves shows about linguistics, right? I'll fight people about less vs fewer, oxford commas, etc., but this episode convinced me that I'm probably in the wrong about "singular they" (substituting "him" or "her" with "they" when referring to non-gendered people). It sounds odd, but we need to move with the times. Trigger warning: American voices, and liberal use of "like" - but they're linguistics experts, so they get to decide what's right and wrong.
<a href="https://soundcloud.com/lingthusiasm/43-the-grammar-of-singular-they-interview-with-kirby-conrod">Find it here</a></dd>
</dl>Installing acoustic panels2020-04-28T00:00:00Zhttps://tomhazledine.com/installing-acoustic-panels/<p>In case it's somehow escaped your attention, I've been having a lot of fun recording a podcast lately. It's a weekly show that we have been running for nearly a year and half now (which is something I'm rather proud of).</p>
<p>This week I installed some acoustic panels in my office to improve the sound quality of the podcast.</p>
<figure><img src="/images/articles/acoustic-panels_unboxing.jpg" alt=""/><figcaption>Unboxing my new acoustic panels</figcaption></figure>
<p>Running our show is very much a hobby, but we take it seriously and aim for the best quality we can achieve. We don't run adverts or have any external funding, so the only budget we have is what we're willing to spend (knowing it's just for fun).</p>
<h2 id="marginal-gains-are-better-than-massive-single-investments" tabindex="-1">Marginal gains are better than massive single investments</h2>
<p>With all that in mind, I <em>do</em> sometimes splash out and "invest" in the 'cast<sup class="footnote-ref"><a href="#fn1" id="fnref1">[1]</a></sup>. I could blow hundreds of pounds on a microphone upgrade, but a more sensible approach is to aim for marginal gains. There are plenty of small changes that can improve audio quality, and most involve spending <em>effort</em> rather than money.</p>
<p>Improving your microphone technique is the biggest thing you can do to improve your audio quality, regardless of the gear you use. And all that takes is a little research and plenty of practice and discipline.</p>
<p>After working on your mic technique, the next best thing you can do is to improve the acoustic qualities of the space you record in. It makes sense: the quality of the sound you capture in you mic is the single biggest factor in the quality of the overall sound. Sometimes you can "fix it in post", but it's far easier, quicker, and cheaper to fix it <em>before</em> you record.</p>
<h2 id="dealing-with-room-noise" tabindex="-1">Dealing with room noise</h2>
<p>So what is your microphone picking up? Two things: your voice, and the sounds of your environment. Hopefully you're able to record in a quiet space with little or no background noise, so it's very likely that the majority of the "room noise" you record will be reflections and reverberations of your own voice.</p>
<p>Every hard, flat surface in your room will bounce you voice back at the microphone. In most cases this manifests as a "boxy" sounding end result. The cure to this problem? Soften the hard surfaces and break up the reflections.</p>
<p>Soft furnishings will help a lot here (sofas, heavy curtains, bookshelves, etc.) but every now and then you find a large, flat surface that you can't do anything else with. That was the case in my new home office - a big flat wall right behind my desk; perfectly poised to reflect the maximum amount of sound directly at my microphone. And that's where acoustic panels come in.</p>
<h2 id="acoustic-panels" tabindex="-1">Acoustic panels</h2>
<p>Acoustic panels are a fairly simple concept: pads of foam that you stick to your wall to reduce reflections. Compared to a lot of audio "gear", they're not too expensive. I picked up a pack of sixteen 30cm x 30cm squares for about £36. But all it is is cleverly shaped foam, so really it's actually <em>quite</em> expensive. But I'm not much of a DIY-er, and I'm generally happy to pay for convenience.</p>
<p>In "proper" studios, you'll find fancy panels with wooden frames, nice fabric covers, and dense rock-wool stuffing. They are obviously more effective than some simple foam glued to the wall, but like I said; my DIY skills are weak.</p>
<h2 id="installation-(maybe)" tabindex="-1">Installation (maybe)</h2>
<p>My first attempt at attaching the foam panels to my wall did not go well. I had a set of double-sided sticky pads, recommended by the Amazon Q&A on the page that I got the panels from. After some careful positioning (with a tape measure and a spirit-level, you'll be pleased to hear), I was pretty pleased with the results. I'd yet to do any recording, but I could test that in the morning...</p>
<p>...jump-cut to the next morning, and many of the panels were strewn across the room or wedged between my computer and the wall. In short - not where I'd left them 🙁</p>
<figure><img src="/images/articles/acoustic-panels_disaster.jpg" alt="" data-alt="Acoustic panels, but most have fallen off the wall"/><figcaption>The sticky pads just weren't sticky enough</figcaption></figure>
<p>The sticky pads were not enough to hold the panels by themselves. The pads had stuck to the wall fine. In fact, if I ever want to take them off I'm sure it'll be a devil of a job! The trouble was that the pads weren't reliably adhering to the foam itself, so the panels would just drop off. Not at first (no! they were happy to lull me into a false sense of security), but gradually, over a period of hours or days, they would fall to the floor.</p>
<h2 id="installation---take-two" tabindex="-1">Installation - take two</h2>
<p>For take-two, I took things up a notch with some serious spray mount. After doing some research, it emerged that the best course of action was to attach the panels to some kind of backing (plywood is best, but cardboard works fine). This time I was taking no chances and both the back of the panel and the cardboard backing got a liberal dousing in spray-fix. I even weighted them down and gave them plenty of time to dry.</p>
<figure><img src="/images/articles/acoustic-panels_combo.jpg" alt=""/><figcaption>Cutting cardboard to act as backing for the panels. Glueing that cardboard in place. Weighing down the glued panels with a heavy box while the glue dries.</figcaption></figure>
<p>Attaching the new cardboard-backed panels to the wall was even quicker and easier than the first attempt. The sticky pads were still attached (and still sticky) so the cardboard adhered very thoroughly. Job done. Or so I thought...</p>
<h2 id="installation---take-three%3F" tabindex="-1">Installation - take three?</h2>
<p>Thankfully the cardboard is still stuck fast. That theory, at least, was sound. And this time <em>most</em> of the panels have stayed in place. In general the glue fixing the foam to the cardboard is still pretty strong, but a couple of panels fell down overnight. Cue <em>day three</em> of trying to get these darned things to stay up 😬</p>
<h2 id="useful-advice" tabindex="-1">Useful advice</h2>
<p>If you're planning on installing acoustic foam panels, the biggest tip I can give is this: apply a sturdy backing to the foam, and <em>make sure it's stuck fast</em> before attempting to attach it to the wall. It's save you a lot of bother.</p>
<p>But was it all worth it? Stay tuned to future episodes of the <a href="">A Question of Code</a> podcast and see if <em>you</em> can hear the difference...</p>
<figure><img src="/images/articles/acoustic-panels_complete.jpg" alt="" data-alt="All the acoustic panels successfully attached to the wall"/><figcaption>The end result</figcaption></figure>
<hr class="footnotes-sep"/>
<section class="footnotes">
<ol class="footnotes-list">
<li id="fn1" class="footnote-item"><p>One of the first signs you've become a "podcast idiot" is you start calling them 'casts. <a href="#fnref1" class="footnote-backref">↩︎</a></p>
</li>
</ol>
</section>CSS Naked Day2020-04-09T00:00:00Zhttps://tomhazledine.com/css-naked-day/<p>As a developer who came of age at the dawn of Web 2.0, supporting semantic markup is something that I've always been in favour of. I've written in the past about how <a href="https://www.tomhazledine.com/html-doesnt-need-any-styling/">HTML doesn't need (much) styling</a> to be perfectly functional. So I'm really enjoying the fact that today (April 9th) is "officially" <em><a href="https://css-naked-day.github.io/">CSS Naked Day</a></em>.</p>
<p>If you're reading this on the 9th, then my whole site will be untouched by CSS. If you're reading on any other day, then it's only <em>this</em> page that's unstyled.</p>
<p>Apart from being a fun exercise, there is a serious point to be made here. A lot of people experience the web via nothing more than the markup (be that through assistive tech like screen readers, or just automated delivery mechanisms like web-scrappers and RSS feeds). The way we structure our documents <em>matters</em>.</p>Twitter Cards with Nunjucks and 11ty2020-02-17T00:00:00Zhttps://tomhazledine.com/twitter-cards-with-nunjucks-and-eleventy/<p>When sharing links on Twitter, there's a handy feature to make those links look prettier. Twitter "cards" will turn your simple text link into a rich card complete with an image and structured title and description data. Here's a recent tweet from my podcast's<sup class="footnote-ref"><a href="#fn1" id="fnref1">[1]</a></sup> account. When writing the tweet, we simply included the url <code>https://aquestionofcode.com/52-what-gear-do-you-use/</code> in the text, and Twitter automatically added the card content.</p>
<blockquote class="twitter-tweet"><p lang="en" dir="ltr">🎙️52: What gear do you use?<br/><br/>This week we take a look at all the tech and tools that we use everyday. <a href="https://twitter.com/hashtag/techgear?src=hash&ref_src=twsrc%5Etfw">#techgear</a> <a href="https://twitter.com/hashtag/techpodcast?src=hash&ref_src=twsrc%5Etfw">#techpodcast</a> <a href="https://twitter.com/hashtag/CodeNewbie?src=hash&ref_src=twsrc%5Etfw">#CodeNewbie</a><br/><br/>Available on your podcatcher of choice!<a href="https://t.co/xRyxuXAAW2">https://t.co/xRyxuXAAW2</a></p>&mdash; A Question Of Code (@aQoCode) <a href="https://twitter.com/aQoCode/status/1234767283710517249?ref_src=twsrc%5Etfw">March 3, 2020</a></blockquote> <script async="async" src="https://platform.twitter.com/widgets.js" charset="utf-8"/>
<p>There are several types of card, including <strong>summary</strong>, <strong>player</strong>, and <strong>summary with large image</strong>. They all serve different purposes and are activated in different ways. You can find the full details on <a href="https://developer.twitter.com/en/docs/tweets/optimize-with-cards/overview/summary">Twitter's developer overview</a>.</p>
<h2 id="adding-support-for-twitter-cards-to-your-own-site" tabindex="-1">Adding support for Twitter Cards to your own site</h2>
<p>Does this happen to all links? Sadly not. To activate this feature, the page you're linking to needs to include some Twitter-specific metadata in it's <code><head></code>. Here's what that metadata for <em>this</em> page looks like:</p>
<pre><code class="language-html"><meta name="twitter:card" content="summary" />
<meta name="twitter:creator" content="{{site.authorTwitterUrl}}" />
<meta property="og:url" content="{{site.url}}{{page.url}}" />
<meta property="og:title" content="{{title}}" />
<meta
property="og:description"
content="{% if excerpt %}{{excerpt}}{% else %}{{site.summary}}{% endif %}"
/>
<meta name="twitter:image" content="{{site.url}}/images/pages_stack_bg.jpg" />
<meta property="og:image" content="{{site.url}}/images/pages_stack_bg.jpg" />
</code></pre>
<h2 id="adding-the-metadata-in-11ty" tabindex="-1">Adding the metadata in 11ty</h2>
<p>My site is built with <a href="https://www.11ty.dev/">Eleventy</a> (a.k.a. <em>11ty</em>), and I use the <a href="https://mozilla.github.io/nunjucks/">Nunjucks</a> templating engine.<sup class="footnote-ref"><a href="#fn2" id="fnref2">[2]</a></sup> I chose Nunjucks because it's the de-facto standard in all the code example in the 11ty docs, and it's about as lightweight and intuitive as any other templating language.</p>
<p>That same block of metadata that I showed before looks like this in a Nunjucks file:</p>
<pre><code class="language-html"><meta name="twitter:card" content="{{cardType}}" />
<meta name="twitter:creator" content="{{site.authorTwitterUrl}}" />
<meta property="og:url" content="{{site.url}}{{page.url}}" />
<meta property="og:title" content="{{cardTitle}}" />
<meta
property="og:description"
content="{% if excerpt %}{{excerpt}}{% else %}{{site.summary}}{% endif %}"
/>
<meta name="twitter:image" content="{{cardImage}}" />
<meta property="og:image" content="{{cardImage}}" />
</code></pre>
<p>Because the way 11ty templates and layouts are put together, I had to add the metadata to my site's master <code>main.njk</code> layout file. This is the main layout that wraps all my pages, so I need to include some custom logic to account for a few scenaris:</p>
<ul>
<li>Pages that don't have an image of their own (most pages on my site, in fact) fall back to using a generic site-logo image.</li>
<li>If we're using the generic site-logo image, I switch the card's <code>type</code> from <code>summary_with_large_image</code> to simply <code>summary</code>, which creates a smaller card that is better suited to a icon or simple logo.</li>
</ul>
<h2 id="gotchas" tabindex="-1">Gotchas</h2>
<p>I have to confess that it took me several attempts to get the images loading correctly<sup class="footnote-ref"><a href="#fn3" id="fnref3">[3]</a></sup>. Twitter provides a helpful <a href="https://cards-dev.twitter.com/validator">Card validator</a>. Having access to that meant I could test and debug without having to spam my followers with tweets full of broken card links (which was a relief, I can say). It still needs a valid <em>real-world</em> url to work, however, and that's not something my current dev-setup can provide for this site (where I normally just rely on a <code>localhost</code> url for testing and development). I just YOLO'd it by repeatedly deploying to my Netlify instance. Probably not "best practice", but it got the job done.</p>
<hr class="footnotes-sep"/>
<section class="footnotes">
<ol class="footnotes-list">
<li id="fn1" class="footnote-item"><p><strong>#shamelessPlug</strong><br/>A podcast, you say? Why yes! be sure to check out <a href="https://aqoc.dev">A Question of Code</a> in your podcatcher of choice. <a href="#fnref1" class="footnote-backref">↩︎</a></p>
</li>
<li id="fn2" class="footnote-item"><p>Convienently, the syntax is the same for Liquid (which is the <em>other</em> popular templating language used with Eleventy) <a href="#fnref2" class="footnote-backref">↩︎</a></p>
</li>
<li id="fn3" class="footnote-item"><p>Note to self, Twitter cards don't like relative paths for images! <a href="#fnref3" class="footnote-backref">↩︎</a></p>
</li>
</ol>
</section>The things I use2020-01-17T00:00:00Zhttps://tomhazledine.com/things-i-use/<p>Every now and then I get "nerd sniped". Something (a link, an idea, anything) comes out of left-field and instantly becomes an obsession. Take, for example, <a href="https://www.decisionproblem.com/paperclips/">Universal Paperclips</a>. I stumbled on that link who-knows-where, and BAM! Three days' productivity lost, just like that.</p>
<p>Thankfully the most recent example is more benign. Wes Bos' <a href="https://uses.tech/">uses.tech</a> site is a catalogue of tech bloggers' "uses" pages. I saw that, and had to drop everything to make my own: <a href="https://tomhazledine.com/uses/">tomhazledine.com/uses</a></p>
<h2 id="what-is-a-%22uses%22-page%3F" tabindex="-1">What is a "uses" page?</h2>
<p>In short, a "uses" page is a list of the things you use. Beyond that, it's really up to you. I've chosen to list the hardware I own and use on a regular basis. I covers the basics like computers and laptops, but also other useful things. I have a section for video- and audio-related tech (which I use for <a href="https://aquestionofcode.com/">podcasting</a> and my occasional <a href="https://www.youtube.com/channel/UCfi3VtksUcc0Xm9xq1-PSdg">coding videos</a>), and also a section that covers my art and design tools (ranging from fancy sketchbooks to The World's Best Pencil<sup class="footnote-ref"><a href="#fn1" id="fnref1">[1]</a></sup>).</p>
<h2 id="future-plans" tabindex="-1">Future plans</h2>
<p>I think having (and maintaining) a <em>Uses</em> page is a valuable thing. I love reading about the tools other professionals use, so hopefully someone else (maybe you!) will find value in my list.</p>
<p>So far I've only scratched the surface (I really love <em>things</em>, it turns out) and there are already plenty of omissions. The tools I use day-to-day fall in and out of favour periodically, so regular updates will be required. And I haven't even started to list the <em>software</em> I use!</p>
<p>I will revisit <a href="https://tomhazledine.com/uses/">my <em>Uses</em> page</a> as often as I can, and will actively seek-out the Uses pages of others. You should make one of your own, and then Tweet the link to me. I'd love to see it!</p>
<hr class="footnotes-sep"/>
<section class="footnotes">
<ol class="footnotes-list">
<li id="fn1" class="footnote-item"><p>It's almost embarrassing how much I love this pencil. The feature set, the weight of it, the finger-feel of the exquisite knurling. Everything about it is perfect. <a href="#fnref1" class="footnote-backref">↩︎</a></p>
</li>
</ol>
</section>Static site generators: Hugo vs Jekyll vs Gatsby vs 11ty2020-01-07T00:00:00Zhttps://tomhazledine.com/eleventy-static-site-generator/<p>I have an obsession with static site generators. They help turn simple markdown files into a lean, mean, super-fast website. They're rendered at compile-time, so there's no server-side activity to slow things down. And because all the browser ever sees is static files, there's much less chance of your site being hacked. My favourite SSG is <a href="https://www.11ty.dev/">Eleventy</a> (11ty), but I've been on a long journey to get here...</p>
<blockquote>
<p><em>I tried <a href="#i-tried-wordpress">WordPress</a>, <a href="#i-tried-vanilla-html">vanilla HTML</a>, <a href="#i-tried-jekyll">Jekyll</a>, <a href="#i-tried-gatsby">Gatsby</a>, and <a href="#i-tried-hugo">Hugo</a>, but <a href="#11ty-is-my-favourite">11ty is my favourite</a></em></p>
</blockquote>
<h3 id="i-tried-wordpress" tabindex="-1">I tried WordPress</h3>
<p>Before I discovered SSGs, I was a WP ninja for years. People often complain about the speed of WordPress, but after running several massive-traffic, high-complexity, image-heavy sites for clients, I'm happy to report that WordPress is not the performance bottleneck that people think it is. (You just have to be very careful!)</p>
<p>I also loved the versatility of <a href="https://developer.wordpress.org/reference/functions/register_post_type/">Custom Post Types</a> and the magic UI that comes with <a href="https://www.advancedcustomfields.com/pro/">Advanced Custom Fields Pro</a>, but never felt that WordPress as a whole aligned with <em>my</em> preferred workflow. Despite the power of CPTs and ACF, the WP dashboard was always a point of friction. It could do <em>too much</em>, while at the same time not being customisable enough 😬</p>
<h3 id="i-tried-vanilla-html" tabindex="-1">I tried vanilla HTML</h3>
<p>Eugh! That was a fun intellectual challenge, and it's something I believe that every frontender <em>should</em> be able to do. But once you get beyond a couple of pages it was a massive pain! I missed Markdown and partials and data-management and, well, all the things people use frameworks and systems for!</p>
<h3 id="i-tried-jekyll" tabindex="-1">I tried Jekyll</h3>
<p>After the over-engineering of WordPress, <a href="https://jekyllrb.com/">Jekyll</a> was a breath of fresh air. Comprehensible flat files! No database to slow things down! Markdown support that makes sense! (Good markdown support is hard to manage in WP even to this day).</p>
<p>Even though I quickly fell out with Jekyll (too much Ruby, not enough flexibility for shared metadata and categorisation, and getting Jekyll to play nicely with my workflow automation was a major PITA), this was the moment that I fell in love with statically generated websites. The concept made perfect sense for the simple blogs I was creating at the time, <em>and</em> meant I jumped straight to the top of the my-site-loads-faster-than-yours leaderboards.</p>
<h3 id="i-tried-gatsby" tabindex="-1">I tried Gatsby</h3>
<p>After I finally mastered React (maybe; it's a long journey, I suspect), it was inevitable that my React-all-the-things! instinct would carry over to my blogs. <a href="https://www.gatsbyjs.org/">Gatsby</a> was an easy sell. All the power of a React SPA, with all the benefits of a statically generated site? Yes, please!</p>
<p>The image-handling and page-prerendering still blow my mind, but in the end it was goddam GraphQL that drove me away.</p>
<blockquote>
<p>The frontmatter's right there! Why do I need to change three files to get it! I don't want to add the data, <em>then</em> add a schema too! Fuuuuuuuuu...</p>
</blockquote>
<p>Needless to say, I've retreated again to less over-engineered pastures.</p>
<h3 id="i-tried-hugo" tabindex="-1">I tried Hugo</h3>
<p>Hugo is awesome, and being able to tell people I use GoLang is fun. But if I'm brutally honest with myself, the street-cred is the only real advantage over Jekyll 😋</p>
<p>It has all Jekyll's benefits, but also all of the same limitations (for me, at least). We use it for our documentation hub at my Jobby Job, and I really enjoy working with it (we might even roll out a Hugo build of our currently-WP brochure site, too). But most of the time I want more control over shortcodes and snippets and advanced features. This could all be improved if I was better at Go, I'm sure, but if there's a JavaScript option out there, that's the one I'll inevitably gravitate towards.</p>
<h3 id="11ty-is-my-favourite" tabindex="-1">11ty is my favourite</h3>
<p>And so we come to Eleventy (a.k.a. <a href="https://www.11ty.dev/">11ty</a>). It has all the benefits of Hugo and Jekyll, but it's written in JavaScript. Flat files? Check! No excess cruft? Check! Markdown is king? Check! I can customise to my heart's content, and add all the shortcodes and layout tweaks I want. The real selling point for me is the intuitive folder structure - it's so simple and elegant, and I grokked it almost instantly.</p>
<p>As an added benefit, it integrates very nicely indeed into my existing workflow automation. I'm not penalised for using WebPack for assets, and relying on GitHub and Netlify for CI is a breeze. I've rebuilt my site many times use many different tools, but the 11ty rebuild was by far the fastest</p>
<p>There are still friction points; pagination's a real pain, as is any attempt to simulate different post types, and the documentation is still a work in process (although that almost makes me <em>more</em> excited because there's a genuine opportunity for me to contribute something meaningful to the project). So, all told, I <em>could</em> still be lured away by a newer and shinier SSG.</p>
<p>But after over a year of use I'm still happy with 11ty. It does the majority of things I'd want a site-generator to do, and gets out of the way when I want to add in my own crazy things. If anything, my travels through the land of SSGs have taught me that the later point is more important - a static site generator <em>cannot</em> get in the way of my workflow. As soon as it does, I'll most likely try another. And there are plenty out there still to try.</p>Rugby prediction: retrospective2019-12-30T00:00:00Zhttps://tomhazledine.com/rugby-prediction-retrospective/<p>Making predictions is a dangerous game, especially if you publicise them. Before this year's (2019's) Rugby World Cup, I was chewing the ear off anyone who'd sit still long enough: "I know how the world cup will turn out. I've made an algorithm!" Not only did I bore all my friends, exhaust my colleagues, and <a href="https://www.tomhazledine.com/rugby-world-cup-predictions/">write up my predictions here on my blog</a>... Not only that, but I actually put my hard-earned money on the line. So confident was I in my predictions, I made a series of bets. "Sure things", I thought them.</p>
<h2 id="pride-comes-before-a-fall" tabindex="-1">Pride comes before a fall</h2>
<p>As anyone who saw my predictions will know already, my algorithm was no match for reality. After a perfect first weekend, things began to unravel from the second weekend of matches. I became very clear very quickly that my predictions were <em>way</em> off.</p>
<p %="" include="" rugbybad-pool.njk%="">
<h2 id="so-what-went-wrong%3F" tabindex="-1">So what went wrong?</h2>
<ul>
<li>I underestimated the specific impact of the players on the pitch and overestimated the record of the team as a whole. Some (but not all) teams are defined by one or two star players, and subsequently highly susceptible to the whims of injury and individual form.</li>
<li>Japan surpassed all my expectations, proving that the tier system lacks nuance. A large part of my algorithm hinged on the idea of "tiers": the gulf in budget and schedule and player-pool mean that "tier one" teams (like the 6 Nations and SANZAAR) are heavily favoured over "tier two" teams. If nothing else, 2019 was the year a new "tier one" team was born. But Japan's ascendancy aside, the two-tier model has been shown time and time again to be a flawed indicator of performance. I can now see evidence of four tiers, at least. And the big question for my algorithm is this: should the model use tiers as part of the calculation, or are tiers the <em>output</em> of the model?</li>
<li>Past performance is not a reliable indicator of future performance.</li>
<li>Much like the players themselves, training and strategy have a big impact on performance (this is maybe the <em>biggest</em> failing of my algorithm - <em>of course</em> a team's strategy has an impact on their results #facepalm). As much as I may previously dismissed their influence, coaches have a big impact. They sometimes field "experimental" teams in the run up to a big tournament; it helps to blood inexperienced players, and provides a playground for experimentation in game-management and set-pieces. And on top of all this, certain teams will target certain games. England saved some tricks for their (in their eyes) inevitable showdown with New Zealand, South Africa held back some devastating weapons for the final (in a staggering display of self-belief), and Japan poured everything into their pool match against Ireland (who in turn fielded a second-string team of their own, naïvely writing the fixture off as a "sure thing"). If the 2019 world cup has taught me anything, it's that there are no "sure things" on a rugby pitch.</li>
<li>It's easy to mock, but some teams do have a history of choking on big occasions. Not to pick on Ireland, but "Ireland-crash-out-in-the-quarter-finals" has become a potent meme. And how do you factor in the French? They always over-perform at world cups (and almost as inevitably do something mind bogglingly stupid, too).</li>
</ul>
<p>You can't build a reliable algorithm on past scores alone. There are so many other factors that impact how a team performs. You might be able to quantify most (if not all) of them, but that information is not hidden in the scores of past games.</p>
<h2 id="do-i-mind%3F" tabindex="-1">Do I mind?</h2>
<p>Of course not. I might have put real money on the line, but it was (thankfully) money I could afford to lose. In fact, the added excitement of having something at stake in <em>every match</em> makes me think I got my money's worth, and then some. The 2019 world cup was the most surprising and entertaining world cup that I've ever followed. The upsets and close-calls that so confounded my algorithm were the very things that made the whole event such fun.</p>
<h2 id="will-i-continue-to-work-on-my-algorithm%3F" tabindex="-1">Will I continue to work on my algorithm?</h2>
<p>You betcha, I will. Roll on the 2020 Six Nations!</p>Algorithmically predicting the results of the 2019 Rugby World Cup2019-09-07T00:00:00Zhttps://tomhazledine.com/rugby-world-cup-predictions/<p>I've been working on a rugby prediction algorithm for a while now. My basic premise is that the World Rugby rankings are a good indicator of past performance, but don't give the full picture when predicting <em>future</em> results. I've already <a href="https://tomhazledine.com/rugby-world-cup-2019">visualised my predictions based on form alone</a>, and the results look reasonable - so is there room for improvement?</p>
<p>My hypothesis is that a more accurate prediction can be made if we take into account short-term form and past performance against specific opponents.</p>
<p>With this in mind, my new model combines four key metrics:</p>
<ol>
<li><strong>All-time performance</strong> - as shown by current world ranking</li>
<li><strong>Recent Form: tier 1</strong> - how well have the team performed in their last 10 matches against tier 1 opposition.</li>
<li><strong>Recent Form: tier 2</strong> - how well have the team performed in their last 10 matches against tier 2 opposition.</li>
<li><strong>History against opponent</strong> - how well have the team performed against the specific opponent we're predicting the result for?</li>
</ol>
<p>There's obviously plenty more that I can add to this algorithm, but with the World Cup actually starting soon (tomorrow, at time of writing), I've got a hard deadline for "just shipping" whatever I've got.</p>
<h2 id="the-predictions" tabindex="-1">The predictions</h2>
<p %="" include="" rugbyv2.pools.njk%="">
<!-- <div id="rugby-pools"></div> -->Using world ranking to predict the results of the 2019 Rugby World Cup pool stages2019-08-21T00:00:00Zhttps://tomhazledine.com/rugby-world-cup-2019/<p>This year (2019) is a rugby world cup year. I like data visualisation, and I like rugby. So here's my primitive attempt to calculate the results of the pool stages of the 2019 World Cup. My simple heuristic is that <strong>world ranking</strong> (as of Aug 22<sup>nd</sup> 2019) is a predictor of a team's success. In effect, my simple algorithm states that a team with a higher ranking will always beat a team with a lower ranking.</p>
<p>Just here for the predictions? <a href="#rugby-pools">skip to the end</a></p>
<blockquote>
<p>Note: I'm not trying to predict who <strong>will</strong> win a match, just who is <strong>expected</strong> to win. When the inevitable "upsets" occur (like Japan vs. South Africa in 2015), I want to be able to say "wow! They only had a <code>{N}%</code> chance of winning, but they did it!"</p>
</blockquote>
<p>So let's translate that calculation into code:</p>
<pre><code class="language-js">const chanceOfWinning = (teamRank, oppositionRank) => {
const combinedRanks = teamRank + oppositionRank;
const invertedRanking = combinedRanks - teamRank; // Becasue a rank of 1 is the best
const percentage = (invertedRanking / combinedRanks) * 100;
return percentage.toFixed(1); // Round to 1 decimal place
};
chanceOfWinning(2, 13); // 86.7
// NZL are ranked `2`, and ITA are ranked `13`
// Therefore NZL have an 86.7% chance of beating ITA
</code></pre>
<p>With this primitive algorithm, I can produce a <em>"likelihood of winning"</em> percentage for any pairing of teams. And on first inspection it looks pretty good (based on my own <em>subjective</em> opinion of who should win a given match). New Zealand are currently ranked #2 in the world, and should be expected to crush a #13 side like Italy. Samoa and Russia (#16 and #20 respectively) should be a much closer match, but you'd expect Samoa to emerge victorious.</p>
<div class="matches--solo">
<div class="match match--solo">
<span class="match__team">
<span class="match__team-ranking">2</span>nzl
</span>
<div class="match__bar">
<div class="bar__inner bar__inner--0 bar__inner--win" style="width: 86.7%;">
<span class="bar__value">86.7%</span>
</div>
<div class="bar__inner bar__inner--1 bar__inner--loss" style="width: 13.3%;">
<span class="bar__value">13.3%</span>
</div>
<span class="match__bar-divider"/>
</div>
<span class="match__team match__team--last">
ita<span class="match__team-ranking">13</span>
</span>
</div>
<div class="match match--solo">
<span class="match__team">
<span class="match__team-ranking">16</span>sam
</span>
<div class="match__bar">
<div class="bar__inner bar__inner--0 bar__inner--win" style="width: 55.6%;">
<span class="bar__value">55.6%</span>
</div>
<div class="bar__inner bar__inner--1 bar__inner--loss" style="width: 44.4%;">
<span class="bar__value">44.4%</span>
</div>
<span class="match__bar-divider"/>
</div>
<span class="match__team match__team--last">
rus<span class="match__team-ranking">20</span>
</span>
</div>
</div>
<h2 id="there-are-problems-with-using-rank" tabindex="-1">There are problems with using rank</h2>
<p>But this method starts to look a bit shaky when we include the #1 ranked team in the world (a crown recently claimed by Wales at the time of writing). Not because Wales are particularly special, but because this algorithm massively favours lower rankings. I'd expect Wales to crush Uraguay (#19 in the world), but would <em>not</em> expect them to have such an easy time against Australia (ranked #6). The ranking-based algorithm predicts <em>both</em> matches would be walkovers:</p>
<div class="matches--solo">
<div class="match">
<span class="match__team">
<span class="match__team-ranking">1</span>wal
</span>
<div class="match__bar">
<div class="bar__inner bar__inner--0 bar__inner--win" style="width: 95%;">
<span class="bar__value">95%</span>
</div>
<div class="bar__inner bar__inner--1 bar__inner--loss" style="width: 5%;">
<span class="bar__value">5%</span>
</div>
<span class="match__bar-divider"/>
</div>
<span class="match__team match__team--last">
ura<span class="match__team-ranking">19</span>
</span>
</div>
<div class="match">
<span class="match__team">
<span class="match__team-ranking">1</span>wal
</span>
<div class="match__bar">
<div class="bar__inner bar__inner--0 bar__inner--win" style="width: 85.7%;">
<span class="bar__value">85.7%</span>
</div>
<div class="bar__inner bar__inner--1 bar__inner--loss" style="width: 14.3%;">
<span class="bar__value">14.3%</span>
</div>
<span class="match__bar-divider"/>
</div>
<span class="match__team match__team--last">
aus<span class="match__team-ranking">6</span>
</span>
</div>
</div>
<p>And there's another problem with using world rankings. Rankings, by their very nature, are ordinal. By ranking alone, the difference between #1 and #2 is the same as the difference between #2 and #3, and so on... Whereas in reality, some teams are much closer than their mere ranking would suggest.</p>
<h2 id="using-points-rather-than-ranking" tabindex="-1">Using points rather than ranking</h2>
<p>A better metric to use as the base for our calculation would be <em>points</em>. Word Rugby, the sport's governing body, uses a points system to determine the world rankings. These points are based on match performance, and range from zero to one hundred (the top side generally has a rating of somewhere near 90 points). In late August 2019, Wales have 89.43 points and New Zealand have 89.40 - it's tight at the top! Australia are on 84.05 and Uraguay have 65.18 points.</p>
<p>Using <code>points</code> rather than <code>rank</code> changes our algorithm slightly (we no longer need to invert the team's value, as higher points are better).</p>
<pre><code class="language-js">const chanceOfWinning = (teamPoints, oppositionPoints) => {
const combinedRanks = teamPoints + oppositionPoints;
const percentage = (teamPoints / combinedRanks) * 100;
return percentage.toFixed(1);
};
</code></pre>
<p>Plumbing our examples into this calculation produces a much tighter set of matches. The end results are still the same (in this system, a team with higher points will always beat a team with lower points, in just the same way as the team with the better ranking always wins).</p>
<div class="matches--solo">
<div class="match match--solo">
<span class="match__team">
<span class="match__team-ranking">2</span>nzl
</span>
<div class="match__bar">
<div class="bar__inner bar__inner--0 bar__inner--win" style="width: 55.4%;">
<span class="bar__value">55.4%</span>
</div>
<div class="bar__inner bar__inner--1 bar__inner--loss" style="width: 44.6%;">
<span class="bar__value">44.6%</span>
</div>
<span class="match__bar-divider"/>
</div>
<span class="match__team match__team--last">
ita<span class="match__team-ranking">13</span>
</span>
</div>
<div class="match match--solo">
<span class="match__team">
<span class="match__team-ranking">16</span>sam
</span>
<div class="match__bar">
<div class="bar__inner bar__inner--0 bar__inner--win" style="width: 51.6%;">
<span class="bar__value">51.6%</span>
</div>
<div class="bar__inner bar__inner--1 bar__inner--loss" style="width: 48.4%;">
<span class="bar__value">48.4%</span>
</div>
<span class="match__bar-divider"/>
</div>
<span class="match__team match__team--last">
rus<span class="match__team-ranking">20</span>
</span>
</div>
<div class="match">
<span class="match__team">
<span class="match__team-ranking">1</span>wal
</span>
<div class="match__bar">
<div class="bar__inner bar__inner--0 bar__inner--win" style="width: 57.8%;">
<span class="bar__value">57.8%</span>
</div>
<div class="bar__inner bar__inner--1 bar__inner--loss" style="width: 42.2%;">
<span class="bar__value">42.2%</span>
</div>
<span class="match__bar-divider"/>
</div>
<span class="match__team match__team--last">
ura<span class="match__team-ranking">19</span>
</span>
</div>
<div class="match">
<span class="match__team">
<span class="match__team-ranking">1</span>wal
</span>
<div class="match__bar">
<div class="bar__inner bar__inner--0 bar__inner--win" style="width: 51.6%;">
<span class="bar__value">51.6%</span>
</div>
<div class="bar__inner bar__inner--1 bar__inner--loss" style="width: 48.4%;">
<span class="bar__value">48.4%</span>
</div>
<span class="match__bar-divider"/>
</div>
<span class="match__team match__team--last">
aus<span class="match__team-ranking">6</span>
</span>
</div>
</div>
<p>These results look a little better than the ranking-only method. The delta between WAL/URA and WAL/AUS looks more realistic, and whoever is in the #1 spot has less of an unfair advantage. But now the <em>amounts</em> look wrong. Any theory that gives Italy a 44.6% of beating New Zealand <strong>must</strong> be inaccurate.</p>
<h2 id="increasing-the-weighting" tabindex="-1">Increasing the weighting</h2>
<p>The points-based system is a better reflection of the team's relative chance of winning, but to my eyes the results aren't extreme enough. It gives too much credit to the lower-tier teams, and not enough to the top-tier ones. For the calculation to better match my expectations, it needs to favour the teams at the top of the rankings. Not only that, but it needs to do it <em>progressively</em> - so a team in the middle gets a bit of a boost, but not as much as those at the top get.</p>
<p>I need to write a function that will adjust the points value of each team. The easiest way to get the result I'm after is to multiply each team's points by a power.</p>
<pre><code class="language-js">const adjustment = num => Math.pow(num, 5);
</code></pre>
<p %="" include="" rugbyrank-graph.njk%="">
<!-- <div id="rugby-rank-graph"></div> -->
<pre><code class="language-js">const chanceOfWinning = (teamPoints, oppositionPoints) => {
const combinedRanks = adjustment(teamPoints) + adjustment(oppositionPoints);
const percentage = (adjustment(teamPoints) / combinedRanks) * 100;
return percentage.toFixed(1);
};
</code></pre>
<p>I started with <code>2</code> as the exponent, and that was better than nothing, but still not enough. <code>10</code> was too extreme, and in the end I settled on <code>5</code>. Increasing each team's points by a power of <code>5</code> gave me a set of probabilities that looked about right. That formula added just enough of a notch in the middle of the graph - and thereby increasing the likelihood of a top-tier team beating a lower-tier one.</p>
<div class="matches--solo">
<div class="match">
<span class="match__team">
<span class="match__team-ranking">2</span>nzl
</span>
<div class="match__bar">
<div class="bar__inner bar__inner--0 bar__inner--win" style="width: 74.6%;">
<span class="bar__value">74.6%</span>
</div>
<div class="bar__inner bar__inner--1 bar__inner--loss" style="width: 25.4%;">
<span class="bar__value">25.4%</span>
</div>
<span class="match__bar-divider"/>
</div>
<span class="match__team match__team--last">
ita<span class="match__team-ranking">13</span>
</span>
</div>
<div class="match match--solo">
<span class="match__team">
<span class="match__team-ranking">16</span>sam
</span>
<div class="match__bar">
<div class="bar__inner bar__inner--0 bar__inner--win" style="width: 57.9%;">
<span class="bar__value">57.9%</span>
</div>
<div class="bar__inner bar__inner--1 bar__inner--loss" style="width: 42.1%;">
<span class="bar__value">42.1%</span>
</div>
<span class="match__bar-divider"/>
</div>
<span class="match__team match__team--last">
rus<span class="match__team-ranking">20</span>
</span>
</div>
<div class="match">
<span class="match__team">
<span class="match__team-ranking">1</span>wal
</span>
<div class="match__bar">
<div class="bar__inner bar__inner--0 bar__inner--win" style="width: 82.9%;">
<span class="bar__value">82.9%</span>
</div>
<div class="bar__inner bar__inner--1 bar__inner--loss" style="width: 17.1%;">
<span class="bar__value">17.1%</span>
</div>
<span class="match__bar-divider"/>
</div>
<span class="match__team match__team--last">
ura<span class="match__team-ranking">19</span>
</span>
</div>
<div class="match">
<span class="match__team">
<span class="match__team-ranking">1</span>wal
</span>
<div class="match__bar">
<div class="bar__inner bar__inner--0 bar__inner--win" style="width: 57.7%;">
<span class="bar__value">57.7%</span>
</div>
<div class="bar__inner bar__inner--1 bar__inner--loss" style="width: 42.3%;">
<span class="bar__value">42.3%</span>
</div>
<span class="match__bar-divider"/>
</div>
<span class="match__team match__team--last">
aus<span class="match__team-ranking">6</span>
</span>
</div>
</div>
<hr/>
<h2 id="results-for-all-the-pools" tabindex="-1">Results for all the pools</h2>
<p>This is of course only based on my experience of rugby and my own highly subjective opinions. But it is still anchored in reality because I'm using the points as a starting point, and treating each team equally (as much as I <em>want</em> to give England a boost, the algorithm doesn't support it).</p>
<p>Ironically, this calculation shows that the draw for this world cup <em>does</em> give England a slight boost. The top 8 teams make it through to the quarter finals as you would expect. But when it comes to the semis, 4<sup>th</sup> ranked South Africa miss out, while 5<sup>th</sup> ranked England manage to sneak in. A side-effect of the pools being drawn years before the event. On the other hand, it probably shows that the draw-process works fairly well if, given all the top 8 make it into the quarters (or at least shows that the rankings have been comparatively static).</p>
<p>I'm not expecting these predictions to come true - there's a lot more to success in rugby than simple rankings. But I do find this kind of objective analysis useful for setting expectations. Looking at these predictions, I'll make more of an effort to see matches I might otherwise have passed on. Tonga vs. USA, for instance, looks like it'll be a close one. As do Scotland vs. Japan and New Zealand vs. South Africa (although after this year's Championship you don't need an algorithm to tell you that'll be a real grudge match!).</p>
<div class="wrapper--rugby">
<div class="pool">
<h3 id="pool-a-matches" tabindex="-1">Pool A matches</h3>
<p %="" include="" rugbypool-a.njk%="">
<h3 id="pool-a-results" tabindex="-1">Pool A results</h3>
<table>
<thead>
<tr>
<th>Pos.</th>
<th>Team</th>
<th>Wins</th>
</tr>
</thead>
<tbody>
<tr>
<td>1st</td>
<td>Ireland</td>
<td>4 wins</td>
</tr>
<tr>
<td>2nd</td>
<td>Scotland</td>
<td>3 wins</td>
</tr>
<tr>
<td>3rd</td>
<td>Japan</td>
<td>2 wins</td>
</tr>
<tr>
<td>4th</td>
<td>Samoa</td>
<td>1 win</td>
</tr>
<tr>
<td>5th</td>
<td>Russia</td>
<td>0 wins</td>
</tr>
</tbody>
</table>
</div>
<div class="pool">
<h3 id="pool-b-matches" tabindex="-1">Pool B matches</h3>
<p %="" include="" rugbypool-b.njk%="">
<h3 id="pool-b-results" tabindex="-1">Pool B results</h3>
<table>
<thead>
<tr>
<th>Pos.</th>
<th>Team</th>
<th>Wins</th>
</tr>
</thead>
<tbody>
<tr>
<td>1st</td>
<td>New Zealand</td>
<td>4 wins</td>
</tr>
<tr>
<td>2nd</td>
<td>South Africa</td>
<td>3 wins</td>
</tr>
<tr>
<td>3rd</td>
<td>Italy</td>
<td>2 wins</td>
</tr>
<tr>
<td>4th</td>
<td>Canada</td>
<td>1 win</td>
</tr>
<tr>
<td>5th</td>
<td>Namibia</td>
<td>0 wins</td>
</tr>
</tbody>
</table>
</div>
</div>
<div class="wrapper--rugby">
<div class="pool">
<h3 id="pool-c-matches" tabindex="-1">Pool C matches</h3>
<p %="" include="" rugbypool-c.njk%="">
<h3 id="pool-c-results" tabindex="-1">Pool C results</h3>
<table>
<thead>
<tr>
<th>Pos.</th>
<th>Team</th>
<th>Wins</th>
</tr>
</thead>
<tbody>
<tr>
<td>1st</td>
<td>England</td>
<td>4 wins</td>
</tr>
<tr>
<td>2nd</td>
<td>France</td>
<td>3 wins</td>
</tr>
<tr>
<td>3rd</td>
<td>Argentina</td>
<td>2 wins</td>
</tr>
<tr>
<td>4th</td>
<td>United States</td>
<td>1 win</td>
</tr>
<tr>
<td>5th</td>
<td>Tonga</td>
<td>0 wins</td>
</tr>
</tbody>
</table>
</div>
<div class="pool">
<h3 id="pool-d-matches" tabindex="-1">Pool D matches</h3>
<p %="" include="" rugbypool-d.njk%="">
<h3 id="pool-d-results" tabindex="-1">Pool D results</h3>
<table>
<thead>
<tr>
<th>Pos.</th>
<th>Team</th>
<th>Wins</th>
</tr>
</thead>
<tbody>
<tr>
<td>1st</td>
<td>Wales</td>
<td>4 wins</td>
</tr>
<tr>
<td>2nd</td>
<td>Australia</td>
<td>3 wins</td>
</tr>
<tr>
<td>3rd</td>
<td>Fiji</td>
<td>3 wins</td>
</tr>
<tr>
<td>4th</td>
<td>Georgia</td>
<td>1 win</td>
</tr>
<tr>
<td>5th</td>
<td>Uraguay</td>
<td>0 wins</td>
</tr>
</tbody>
</table>
</div>
</div>
<!-- <div id="rugby-pools"></div> -->Writing well is essential. Try your best to get good at it2018-07-01T00:00:00Zhttps://tomhazledine.com/writing-well/<p>I’ve worked hard on my writing style, and continually reap the benefits of having done so. Email and instant messages take <em>more</em> work than face-to-face communication, and being able to convey a point clearly and concisely is an essential skill. Writing documentation is even harder, but just as important. I’ve had to navigate poorly documented code in the past, and try my best to ensure no-one has the same experience when trying to decipher code I’ve written.</p>
<p>I’m not saying that I’m a good writer - I’ve got a long long way to go on that front. But over the last few years I’ve definitely improved, and I can quantify that improvement by looking at the decrease in communication-based foul-ups at work.</p>
<p>In the past my communications with clients would often be misinterpreted. I can attribute some of the blame to fact that clients are often fundamentally uninterested in the minutiae of the product they’ve commissioned. Busy CEOs probably don’t need to understand the nuances of browser caching<sup class="footnote-ref"><a href="#fn1" id="fnref1">[1]</a></sup>, but they do need to know how to be sure they’re looking at the correct version of their website. Explaining technical details to people untrained in the technology is a fundamentally hard problem.</p>
<p>There’s no escaping email. The people I need to talk to are busy, and I’m a socially awkward developer who’ll do anything to dodge actually picking up a phone. But I do need to teach the client to use the new features we’ve built for them, and they need to make me understand the problem they’re really having (rather than just the symptom they’re able to see and describe).</p>
<p>Professional communication is a non-trivial problem, but over the years I’ve developed a couple of coping strategies. I’ve had to master two main skills that appear to be mutually exclusive: <em>brevity</em>, and <em>tactical repetition</em>.</p>
<h2 id="keep-your-writing-short" tabindex="-1">Keep your writing short</h2>
<p>Everyone’s heart sinks when they open an email and are presented with an essay. The dreaded “wall of text” will put off even the most keen communicator. Particularly when explaining a fix for a particularly tricky bug, my instinct is to go deep and detailed<sup class="footnote-ref"><a href="#fn2" id="fnref2">[2]</a></sup>. Learning to cut out the unnecessary details and get straight to the point took me a long time. The people I’m writing to aren’t as interested in the nuances as I am. They want to know what they need to do, and how they can do it quickly.</p>
<h2 id="tactically-repeat-the-important-points" tabindex="-1">Tactically repeat the important points</h2>
<p>Alongside this newfound concision, I’ve also got great mileage from learning when to tactically repeat myself. If there’s a particularly complex point to get across, the key is to say the same thing in two different ways. The scope of clients to misinterpret a set of instructions is seemingly limitless, but I’ve found they’re less likely to misinterpret two different sentences <em>in the same way</em>.</p>
<p>Tell them something once, and they’ll just live with whatever garbled interpretation hits their brain first. Tell them the same thing in two different ways, and they’ll either get it straight away (the happy path!) or at the very least notice that their interpretation is confusing. Then they’ll actually tell you they don’t understand, rather than just muddling along.</p>
<p>If they don’t understand but also let you <em>know</em> they don’t understand, that’s fine. Sometimes things need repeating or unpicking (and sometimes my explanations are simply too confusing, and I need to work harder to find the real issue at hand). If these confusions are picked up early, everyone ends up better off.</p>
<p>As a bonus tip, I picked up a useful phrase from the world of solicitors: “For the avoidance of doubt…”. That one comes in handy when you need to repeat something back to someone, but want them to know why you’re doing it. Soften it if you need to, but never hesitate to say “so just to make sure I’ve got this straight…” or something similar.</p>
<p>Using these two strategies has vastly improved my work life; the return on investment from improving my writing is far greater than from learning a new programming language. Being good at your job is, of course, essential. But a marginal gain in writing ability will give better results than a marginal gain in coding ability.</p>
<p>And it doesn’t just help with clients. Most work-place communication (for me) happens over Slack now (other messaging platforms are available). And let’s not forget every developer’s favourite task; creating the docs. Writing good documentation for your code saves hours and hours of developer-time in the long run. It’s not enough to ensure every method has it’s parameters written down: you have to explain how the whole system fits together. Explaining hard things is hard, but we need to do it.</p>
<p>People misunderstand each other all the time, and if it goes unnoticed the consequences can be time consuming, frustrating, and expensive. If I can give one piece of advice to anyone starting out in a technical field, it’s this: work on your communication skills as much as you can. Write, write, write.</p>
<hr class="footnotes-sep"/>
<section class="footnotes">
<ol class="footnotes-list">
<li id="fn1" class="footnote-item"><p>Good luck if you ever have to hear a CEO pronounce “cache”. <a href="#fnref1" class="footnote-backref">↩︎</a></p>
</li>
<li id="fn2" class="footnote-item"><p>As <a href="https://www.youtube.com/watch?v=zSgiXGELjbc">Carl Sagan said</a>, “if you wish to make an apple pie from scratch, you must first invent the universe.” <a href="#fnref2" class="footnote-backref">↩︎</a></p>
</li>
</ol>
</section>You can now install Picobel using NPM2017-06-28T00:00:00Zhttps://tomhazledine.com/you-can-now-install-picobel-using-npm/<p>I’ve finally published Picobel as a node package. Picobel is my open source JavaScript tool for turning HTML <code><audio></code> tags into styleable markup (a.k.a. regular divs and spans that you can target with your CSS).</p>
<p>For a while now it’s been possible to use Picobel by directly including the script in your project (just like all those old-school jQuery plugins). But it’s been a long time since I included a script or lib like that. My <code>package.json</code> rules the roost now, and almost everything I use gets installed with <code>npm install PACKAGE_NAME</code>. No doubt your workflow has advanced in a similar manner. With that in mind, it seemed foolish not to update Picobel to fit in with a more modern development workflow.</p>
<h2 id="what%E2%80%99s-changed%3F" tabindex="-1">What’s changed?</h2>
<p>For the time being, there are no breaking changes to Picobel’s core functionality. If you have any <code><audio></code> tags on a web page, call <code>Picobel();</code> in your script and Picobel will replace those audio elements with markup that you can style directly. The only difference is how you get access to that <code>Picobel</code> function.</p>
<p>You can still include the script directly if you like, but now you’ll need to reference the <code>picobel.legacy.js</code> file instead. But for all you cool cats on the NPM bandwagon, things are much nicer...</p>
<p>Install Picobel in your project with <code>npm install picobel —save</code> and then import it into your JS file with <code>import Picobel from ‘picobel’;</code>. Then you’re free to call the <code>Picobel()</code> function just like before.</p>
<p>For more information about using options and themes with Picobel, take a look at the README.md on the project’s GitHub page.</p>
<h2 id="what%E2%80%99s-on-the-horizon%3F" tabindex="-1">What’s on the horizon?</h2>
<p>This update to Picobel is the precursor to quite a few changes. It’s part one of an ongoing project to convert Picobel to ES6, expand the test coverage to all Picobel’s internal methods, and get around to adding some requested new features and themes.</p>Recommended Listening: my favourite podcasts2017-05-29T00:00:00Zhttps://tomhazledine.com/podcasts-recommended-listening/<p>I have a long commute, so blast through at least a couple of hours of podcasts every day. I enjoy polished efforts like those from Gimlet Media and NPR, but for me the real gold lies in the more unstructured efforts. Without any constraints of format or time, the best podcasts get to dive deep into topics that wouldn't otherwise get covered.</p>
<p>The recent "podcast renaissance" has felt like a vindication. I've been trying to get my peers to listen to podcasts for years (I even made my own podcast back in '06, but the less said about that the better...). Following the mainstream breakthrough of Serial and the prevalence of shows with high production values (like those from Gimlet), the format is finally getting the attention it deserves.</p>
<hr/>
<h2 id="tech-podcasts" tabindex="-1">Tech Podcasts</h2>
<p>As a developer, I'm naturally drawn to the more technical end of the spectrum. I'm happiest when listening to an hour-long discussion about the intricacies of a new file format...</p>
<h3 id="shoptalk-show" tabindex="-1"><a href="http://shoptalkshow.com/">ShopTalk Show</a></h3>
<p>Answering listener questions about front-end web development - alternate topic-based interviews and "rapid fire" Q&A shows.</p>
<p><em>Listen first:</em> <a href="http://shoptalkshow.com/episodes/254-webvr-josh-carpenter-kevin-ngo/">The Web-VR episode</a> is a good example of the general tone of ShopTalk, but the best-ever episode was when host Chris <a href="http://shoptalkshow.com/episodes/special-one-one-hacker/">interviewed the guy who hacked his site</a>.</p>
<h3 id="atp" tabindex="-1"><a href="http://atp.fm/">ATP</a></h3>
<p>Discussion show about all things Apple. If you like geeking-out about file systems and enjoy endless debate about the next Mac Pro, this is the show for you.</p>
<p><em>Listen first:</em> Best to just jump in at the <a href="http://atp.fm/">latest episode</a>, but I they go "big" on WWDC (Apple's developer conference), so don't start with one of those as they're not representative of the rest of the episodes.</p>
<h3 id="codepen-radio" tabindex="-1"><a href="https://blog.codepen.io/radio/">CodePen Radio</a></h3>
<p>The team behind the CodePen web app give a behind the scenes look at what it's like to run a tech startup.</p>
<p><em>Listen first:</em> The <a href="https://blog.codepen.io/2016/07/13/098-animation/">episode about animation</a> makes a good jumping-off point, but they cover a wide range of topics from server-setups to getting VC-funding to hiring to managing an online "community", etc...</p>
<h3 id="a16z" tabindex="-1"><a href="http://a16z.com/podcasts/">a16z</a></h3>
<p>Archetypal Silicon Valley VC firm Andreessen Horowitz (a16z) present discussions on the hot tech topics of the day.</p>
<p><em>Listen first:</em> A recent episode about <a href="http://a16z.com/2017/04/08/culture-open-source-community/">Open Source</a> was pretty good, but the episodes that feature the company founders, Marc Andreessen and Ben Horowitz are worth digging for.</p>
<h3 id="the-creative-coding-podcast" tabindex="-1"><a href="http://creativecodingpodcast.com/">The Creative Coding Podcast</a></h3>
<p>A mix of game-dev tips'n'tricks and insights into the world of hardware hacking with Rpi-zeros, Arduinos, etc. (the clue is in the name!).</p>
<p><em>Listen first:</em> I learnt a lot about how old-school Nintendo light-guns (Duck Hunt FTW!) worked from <a href="http://creativecodingpodcast.com/hacking-classic-nintendo-guns-and-going-viral/">this episode</a>.</p>
<hr/>
<h2 id="startups-%26-business-podcasts" tabindex="-1">Startups & Business Podcasts</h2>
<p>I've long since discarded any genuine plans of entrepreneurship in favour of the life of a happy drone. But somewhere deep down the itch is still there, and I keep it in check by living vicariously through the exploits of others.</p>
<h3 id="startup" tabindex="-1"><a href="https://gimletmedia.com/startup/">Startup</a></h3>
<p>This is the big daddy. Not only a good series about what it's like to start a business, but also a look into the inner machinations of podcasting itself. Meta. The more recent seasons look at other businesses, but season one was all about the founding of Gimlet Media. Host Alex Blumberg had a vision for what a podcast company could be, and documented his journey to get there.</p>
<p><em>First listen:</em> This is a show that requires <a href="https://gimletmedia.com/episode/1-how-not-to-pitch-a-billionaire/">starting at the very beginning</a>. Listening to Alex mess up a pitch to infamous VC Chris Sacca is radio gold, and the whole story is expertly told.</p>
<h3 id="the-tim-ferriss-show" tabindex="-1"><a href="http://tim.blog/podcast/">The Tim Ferriss Show</a></h3>
<p>Tim's approach to life is <a href="https://twitter.com/unsuiii/status/845393423188275200">very easy to mock</a>, and in many ways I find him a frustrating person to listen to. Yet he obliquely manages to get more out of his guests than most interviewers. He asks the questions I'd like to ask, and always pushes for more detail when I want to hear more detail.</p>
<p><em>First listen:</em> The show works best when Tim gets total buy-in from his guest. The Seth Godin episodes are always good listens, but Seth is all about inspiring great work in others. I prefer to be inspired by stories straight from the horses mouth, so my favourite episode so far is probably the first <a href="http://tim.blog/2015/02/09/matt-mullenweg/">Matt Mullenweg interview</a>.</p>
<h3 id="planet-money" tabindex="-1"><a href="http://www.npr.org/podcasts/510289/planet-money">Planet Money</a></h3>
<p>This one straddles the line between "business" and "general interest". The stories are always interesting, and always have an economics angle to them. Much less "actionable" that the insights you get from the Tim Ferriss Show, for instance, but learning more about how the world around us works never did me any harm.</p>
<p><em>First listen:</em> Episode #762 "The Fine Print" tells three short stories about what happens when you actually read the fine print. All the episodes are short, but generally tell one story. This trio serves as a nice potted introduction to the show's style.</p>
<h3 id="boagworld" tabindex="-1"><a href="https://boagworld.com/show/">Boagworld</a></h3>
<p>I used to class this show as a tech podcast, but in reality it's much more focused on the mechanics of running a web business. Regular hosts Paul and Marcus are joined by a revolving door of guests, and each season has an over-arching theme that guides the conversations.</p>
<p><em>First listen:</em> The <a href="https://boagworld.com/season/15/">15<sup>th</sup> season</a> was all about digital project management. When the season-wide topic was announced I groaned - could they have found a more boring subject to drag out over thirteen episodes?! To my surprise, however, it lead to a whole series of fascinating discussions.</p>
<hr/>
<h2 id="general-interest-podcasts" tabindex="-1">General Interest Podcasts</h2>
<p>I'm not a 100%-mission-focused robot, so I do occasionally listen to podcasts for nothing more than the sheer enjoyment of it.</p>
<h3 id="reply-all" tabindex="-1"><a href="https://gimletmedia.com/reply-all">Reply All</a></h3>
<p>New episodes of Reply All jump straight to the top of my "must listen" playlist. The subject matter is</p>
<p><em>First listen:</em> The episode on phishing (appropriately called <a href="https://gimletmedia.com/episode/97-what-kind-of-idiot-gets-phished/">"What Kind of Idiot Get's Phished?"</a>) was a real eye-opener, but was presented by one of the show's producers. The long-term charm of the show comes down to the interactions between regular hosts Alex & PJ, so listen to a few episodes to get a true flavour.</p>
<h3 id="more-or-less" tabindex="-1"><a href="http://www.bbc.co.uk/programmes/b006qshd">More or Less</a></h3>
<p>In a similar vein to Planet Money, but a little more irreverent and more interested in unpicking topical issues. A look at the numbers and statistics in the news and in life.</p>
<p><em>First listen:</em> A recent episode on the <a href="http://www.bbc.co.uk/programmes/p0500yjw">Economics of Overbooking</a> provided an interesting perspective on the United scandal (where a poor passenger was assaulted by staff on an overbooked flight).</p>
<h3 id="to-the-f'ing-future" tabindex="-1"><a href="https://www.rocketjump.com/podcasts#tothefnfuture">To The F'ing Future</a></h3>
<p>Epitome of American "Bros" talking nonsense, but some interesting discussions if you can get past the personalities. The basic idea is that they talk about "the future" of a different topic each week. Had a short run in 2013 and then stopped, which was a shame.</p>
<p><em>Listen first:</em> <a href="https://www.rocketjump.com/listen/episode-5-rob-auten-video-game-writer">Episode #5 (with Rob Auten)</a> is an interesting look at what the state-of-the-art in video-game plot and characterisation was like in 2013, and what they thought the future would hold. Even more interesting when viewed from 2017; we can see what played out and what sank without trace. Episode #6 about tattooing was also fascinating.</p>
<h3 id="song-exploder" tabindex="-1"><a href="http://www.maximumfun.org/song-exploder">Song Exploder</a></h3>
<p>A geeky behind-the-scenes look at specific songs. Uses the original mixer-tracks to dig into how a song was constructed (often looking at early demos etc., too). If any of you remember the TV show Classic Albums, this is a lot like that but just for a single song.</p>
<p><em>Listen first:</em> <a href="http://www.maximumfun.org/song-exploder/song-exploder-no-22-books">Episode #22: "Smells Like Content" by The Books</a></p>
<h3 id="in-our-time" tabindex="-1"><a href="http://www.bbc.co.uk/programmes/articles/598SVYJ2smP8qJlpH29y7Vj/podcasts">In Our Time</a></h3>
<p>The BBC at its best: three experts around a table explaining a topic to Melvin Bragg. A normal Radio 4 programme, but having the archive available as podcasts is a fantastic resource. I tend to find the maths ones the most interesting, but part of the appeal is being introduced to a topic I'd never have learnt about otherwise.</p>
<p><em>Listen first:</em> Take your pick - the topics are so varied it's hard to mark any individual episode as "representative". The <a href="http://www.bbc.co.uk/programmes/articles/598SVYJ2smP8qJlpH29y7Vj/podcasts">IOT archive</a> is a great place to start browsing.</p>Introducing Picobel.js - an audio player you can style with css2017-04-30T00:00:00Zhttps://tomhazledine.com/picobel-js-css-audio-player/<p>I've launched <a href="https://audio.tomhazledine.com">my first open source project</a>. Well, okay, so it's not my first and it's also not 100% launched yet. Maybe I should explain...</p>
<h2 id="what-is-picobel.js%3F" tabindex="-1">What is Picobel.js?</h2>
<p><a href="https://audio.tomhazledine.com/">Picobel.js</a> (pronounced <em>peek-o-bell</em>, as in <em>decibel</em>) is a lightweight dependency-free Javascript tool that converts html <code><audio></code> tags into styleable markup.</p>
<h2 id="why-i-built-it" tabindex="-1">Why I built it</h2>
<p>Back in the day, I ran a music-review blog<sup class="footnote-ref"><a href="#fn1" id="fnref1">[1]</a></sup> that made heavy use of on-page audio players. At the time I was also diving deep into css. If you could style it, I would style it. Few things were more satisfying than getting a raw html element to look exactly how I wanted it to look. Before long most elements were bending to my will. Checkboxes and select inputs presented no problems. Range selectors were tricky, but eventually looked exactly how I wanted them. But the <code><audio></code> element always eluded me.</p>
<p>Picobel is my attempt to get around the restrictions of the native html5 audio player. In it's simplest form, Picobel finds any audio elements on a page and replaces them with "regular" markup (<code>div</code>s and <code>spans</code> etc.). I can then apply my own styles, and avoid the cross-browser inconsistencies of the "shadow DOM".</p>
<p>For good measure I've also included a selection of optional "themes". If css trickery isn't something that excites you, then you can pick a pre-made style and use that.</p>
<h2 id="my-open-source-debut" tabindex="-1">My open source debut</h2>
<p>Ever since I first joined GitHub I've been storing the code for my personal projects "in the open". But there is a big difference between technically public and actually <em>launching</em> something. This is the first time I've moved beyond "something that works for me" towards "something others can use".</p>
<p>With Picobel.js I've thought a lot about <em>ease of use</em>. Rather than being tightly coupled to my esoteric workflow, it works in any situation. I've also made a conscious effort to document my process. Rather than needing to examine the inside of my head, potential users can now simply <a href="https://github.com/tomhazledine/picobel">read the docs</a>.</p>
<p>Picobel has a demo page complete with detailed installation instructions. There's also a CodePen collection that serves as a gallery for all the pre-made themes<sup class="footnote-ref"><a href="#fn2" id="fnref2">[2]</a></sup>. The GitHub project includes an official open source license (the MIT one, for now). And there is a widget built into the README that shows the current build-status. That's right; this project even has tests!</p>
<h2 id="how-far-off-is-version-1.0%3F" tabindex="-1">How far off is version 1.0?</h2>
<p>In my mind Picobel is <em>almost</em> feature-complete. The only gaps I can think of involve edge-cases and failure states. The "buffering" state is currently rather primitive. There's also no catch for when a file fails to load completely. But I'm happy to save these enhancements for Version 2.0. As it stands, Picobel <em>works</em>. It's much better to get it published and in the hands of users.</p>
<p>So Picobel is on the cusp of hitting the 1.0 milestone. I "soft launched" it on Reddit a couple of weeks ago and gained a few early users. With another week or so of beta testing I'll be ready to call version one of Picobel <em>finished</em>. Now I need your help. If you could <a href="https://audio.tomhazledine.com/">try out Picobel</a> and let me know what you think (good or bad), I'll be eternally grateful.</p>
<hr class="footnotes-sep"/>
<section class="footnotes">
<ol class="footnotes-list">
<li id="fn1" class="footnote-item"><p><a href="http://eatenbymonsters.com">Eaten by Monsters</a> ran for about five years. Slowly customising every aspect of that site is what drew me into web-development. So it's got a lot to answer for... <a href="#fnref1" class="footnote-backref">↩︎</a></p>
</li>
<li id="fn2" class="footnote-item"><p>Picobel lets developers create <em>their own</em> css styles for audio players. But by providing a range of pre-made themes the utility of this project is much increased. Having a "plug and play" option allows people to get results without mucking around with any css first. <a href="#fnref2" class="footnote-backref">↩︎</a></p>
</li>
</ol>
</section>I changed my site's font to Comic Sans as an April Fool. It was a disaster.2017-04-19T00:00:00Zhttps://tomhazledine.com/comic-sans-april-fool-disaster/<p>I like to think of myself as a mature typographer<sup class="footnote-ref"><a href="#fn1" id="fnref1">[1]</a></sup>. I've spent years obsessing of the minutia of line-lengths and leading and kerning-pairs. <a href="http://amzn.to/2o3zgne">Bringhurst's <em>Elements</em></a> is always within arm's reach of my desk, but of course it doesn't need to be because I've got it memorized. I've even reached <em>acceptance</em> in the <a href="https://en.wikipedia.org/wiki/K%C3%BCbler-Ross_model">Kübler-Ross <em>5-stages</em></a> when it comes to Comic Sans.</p>
<ol>
<li><strong>Denial</strong> - "Nobody will use Comic Sans now that there are so many good alternatives to choose from."</li>
<li><strong>Anger</strong> - "Why are all these idiots still using Comic Sans?!"</li>
<li><strong>Bargaining</strong> - "Let's popularise tools that erase Comic Sans from people's computers. If it's not available as a default, they won't use it."</li>
<li><strong>Depression</strong> - "Why do we even bother designing things? Most people will choose Comic Sans. I don't want to live on this planet anymore."</li>
<li><strong>Acceptance</strong> - "Comic Sans is actually a well-constructed typeface, and is appropriate in certain situations."</li>
</ol>
<h2 id="making-the-change" tabindex="-1">Making the change</h2>
<p>For this year's April Fools' Day I thought I'd take that acceptance one step further. Throwing caution to the wind, I set all the text on my website in Comic Sans. Thanks to my fancy front-end workflow, it was a simple task. My styles are all pre-compiled with SASS, so things like colours and font-families are all declared as <code>$variables</code><sup class="footnote-ref"><a href="#fn2" id="fnref2">[2]</a></sup>.</p>
<pre><code class="language-css">// Fonts
$text: "ff-tisa-web-pro", georgia, serif;
$code: Menlo, Monaco, "Andale Mono", "Lucida Console", "Courier New", monospace;
</code></pre>
<p>Changing to Comic Sans (or more specifically <code>Comic Sans MS</code>) meant swapping a couple of lines of code.</p>
<pre><code class="language-css">// Fonts
$text: "Comic Sans MS", cursive, sans-serif;
$code: "Comic Sans MS", cursive, sans-serif;
</code></pre>
<p>My front-end tooling makes pushing these changes to my live site a quick process, too. I committed my "hilarious" update into GIT and pushed that to the remote repository. Then I connected to my server by <code>ssh</code>, ran <code>git pull</code> to get the changes from the repo. The final step was to run <code>gulp sass</code> to compile the production-ready CSS. Writing it all down like that makes it look more complicated than it is. Once you've run through the process a couple of times, deploying an update takes <5 minutes. Overkill for a quick two-liner like this, perhaps, but it works for updates of any size.</p>
<figure class="post-content__image-wrapper">
<img class="post-content__image" src="/images/articles/comic-sans-screenshot.png" alt="My site, but set in Comic Sans."/>
<figcaption class="post-content__caption">My site, but set in Comic Sans.</figcaption>
</figure>
<h2 id="a-bit-of-background" tabindex="-1">A bit of background</h2>
<p>I'm by no means the first person to have a little fun at the expense of Comic Sans' terrible reputation. In 2012 CERN spokesperson Fabiola Gianotti released some results from the ATLAS project. There was an <a href="http://www.theverge.com/2012/7/4/3136652/cern-scientists-comic-sans-higgs-boson">outcry from type-nerds</a> because the slide-deck for the announcement was set entirely in Comic Sans. Yes, you heard right: the discovery of the Higgs Boson was announced in Comic Sans.</p>
<p>CERN had the last laugh a couple of years later. In 2014 they announced they had rebranded - and the new official typeface of the European Organization for Nuclear Research was Comic Sans. They made the announcement <a href="https://home.cern/about/updates/2014/04/cern-switch-comic-sans">on their website</a> - and of course the entire site was set in Comic Sans. The date of that announcement, you ask? April the 1<sup>st</sup>.</p>
<h2 id="what-i-learned-from-this" tabindex="-1">What I learned from this</h2>
<p>My little prank was <em>not</em> a resounding success. I got a couple of "tears-of-joy" emoji when I released the update - which is all I was hoping for, in fairness. But it soon became clear that it only worked when visitors to my site already had Comic Sans MS installed on their machines. Visit from an iOS device, and all you'd see were the fallback fonts.</p>
<p>If I'm honest this came a quite a shock, but wasn't all that surprising once I thought about it. I'd been working on the assumption that all the classic "web safe" system fonts from my youth would still <em>be</em> web safe. I hadn't taken into account how much the web ecosystem has changed since Apple became one of the dominant hardware providers. Inevitably Apple were too "design sensitive" to bundle the much-maligned Comic Sans with their mobile operating system.</p>
<p>The practical upshot of this is slightly ironic; if I wanted my prank to be truly cross-browser I'd have to use a Comic Sans MS web font.</p>
<h2 id="a-good-design-should-work-with-any-typeface" tabindex="-1">A good design should work with any typeface</h2>
<p>I'm pleased to report that even when clad in Comic Sans this site remained lean and functional. I even tried switching to Comic Sans on a couple of our more visual projects, and they too looked, well... okay. It certainly made for a humourous minute or two in the office. And the designs <em>did</em> still hold up.</p>
<p>I often hear that "good design" works in all situations, no matter what typeface you wrap it in. The reality, I think, is a little more nuanced. The typeface always sets the tone for the content. And on a site like mine the typeface has a massive impact because most of the content is <em>words</em>.</p>
<hr class="footnotes-sep"/>
<section class="footnotes">
<ol class="footnotes-list">
<li id="fn1" class="footnote-item"><p>Although I'm now a full time developer and haven't been an "official" designer for years. Typography is a crucial part of my job, but makes up a much smaller percentage of my time than it used to: 30% > ~5% <a href="#fnref1" class="footnote-backref">↩︎</a></p>
</li>
<li id="fn2" class="footnote-item"><p>To keep down requests and page-size I only use one custom web-font, <a href="https://typekit.com/fonts/ff-tisa">FF Tisa</a>. All the fallbacks in my font-stack are system-fonts. I'm not too fussy about <em>which</em> monospace gets used in <code><code></code> blocks, so I've stuffed that with "nice to haves". <a href="#fnref2" class="footnote-backref">↩︎</a></p>
</li>
</ol>
</section>Living with Alexa: the problems with "voice" as an interface2017-03-31T00:00:00Zhttps://tomhazledine.com/echo-alexa-voice-interface-problems/<p>Alexa represents a growing trend. The big tech players are investing heavily in voice-control (in order of current competence: Amazon, Google, Microsoft, Apple). They see voice as the next step logical interface for technology. Gone are they days, they argue, when we had to do anything quite so quaint as type on a keyboard. And in many ways, they're right: being able to say what you want and get an immediate answer can be fantastically useful. But it doesn't always beat a good-old-fashioned keyboard.</p>
<p>I've lived with Alexa (the virtual assistant held within the Amazon Echo) for six months now. The experience has changed how I feel about voice-as-an-interface. Here are a few of my observations:</p>
<ul>
<li>Failure states need a lot of work, and Alexa hasn't got it right yet. The "I'm sorry, I'm having trouble understanding right now" message gets more annoying than a 404 or 500 error page ever does. Also, defaulting to a web-search is not the best option for most queries. Precisely when you didn't want to have to mess about, the interaction becomes <em>longer</em> and <em>more</em> complicated.</li>
<li>Text input is often more precise (and certainly more concise). Natural Language Processing has its limits, and sometimes you don't want the hassle of converting your abstract thought into prose.</li>
<li>You can't browse as easily - it's quicker to read than to listen, and easier to bounce-between and assess options. You can't "skim-listen".</li>
<li>Half the time you feel like an idiot talking aloud to your devices.</li>
<li>It's not discreet: I often want to multi-task, particularly in boring meetings. (This is more of a future-worry, as I currently only have voice-enabled stuff in my house.)</li>
<li>Alexa isn't able to distinguish between individual voices. We want (and expect) her to work in all scenarios: whether we've got a cold, or are whispering, or shouting, or in the next room. As a result, natural-language interpretation is very good, but it responds to <em>any</em> voice. I think Alexa needs to know who her "boss" is...</li>
<li>Following on from that, security is non-existent. My friends can hijack the party playlist. Adverts can inadvertently purchase things from Amazon (this has not happened to us yet, but Alexa has woken up because of wake-words being spoken on the telly).</li>
<li>These is a palpable shortage of "skills" (the Alexa-speak term for what other platforms would call apps), and the ones that are available are either very clunky or too US-centric. A16Z analyst <a href="http://ben-evans.com/newsletter">Benedict Evans</a> thinks it's not surprising that most 'skills' go unused. “It's hard enough to remember what apps you have when you can see them on a screen - having to remember to ask for them by name is the new command line”.</li>
<li>Amazon are doing their best to kick-start a "skills" ecosystem. They have an enthusiastic developer-outreach programme with a big marketing budget (based on the targeted adds I can't seem to escape from...). They've also opened up the technology, so developers can run the (Java-based) Alexa code on their own machines.</li>
<li>Music has been the killer app for us. Being able to select a song, pause, and skip without having to open any apps or type anything is great. It's eliminated a friction I didn't even know was there. Selecting music manually now feels archaic. Porting my (large) music library into Amazon Music was a pain (and I'm still not finished syncing everything). It also cost £25 to get enough storage, but being able to select specific artists and songs was totally worth it. The default music library you get with Prime is fairly comprehensive, but Amazon definitely wants us all to sign-up for their music streaming service (not my bag at all).</li>
<li>I was surprised how often I'd simply say “Alexa, play some music”. After a little training period (“Alexa, I don't like this!”) the resulting playlists were actually pretty good.</li>
<li>The "always on" nature of the Echo, while creepy, is one the most game-changing aspects. It's the factor that tips Alexa from a novelty to a utility. I just speak into the air, and she responds. No boot-up time, no log-in required. She just works.</li>
<li>The speech detection is excellent. We have the Echo in the living room (tucked behind the telly) and it can still hear me when I'm in the kitchen, even when there are other people in both rooms. Alexa gets easily confused when multiple people try to talk at once (or talk over each other, which happens a lot when the family are visiting). But I get easily confused when that happens too, so I can't judge the Echo too harshly on this score.</li>
<li>Getting answers to trivia & general-knowledge questions might be the most useful feature, but the music features are still more impressive when showing off to friends.</li>
</ul>What is a decibel, anyway?2017-03-22T00:00:00Zhttps://tomhazledine.com/what-is-a-decibel-anyway/<p>The decibel has always confused me. Sometimes people use decibels as an absolute value where "0" is silence and anything over "120" is very loud. Journalists do this when describing how loud a band is<sup id="fnref-1"><a href="#fn-1" class="footnote-ref">1</a></sup>. Sound engineers do it too, when boasting about the power of their speaker-setup. "It's louder than a jet engine at 50 paces, man". But when I look at the volume meter on a mixing desk, things are different. The desk shows the loudest value as zero, all the way down to silence at "-96" (or sometimes even "∞"). See what I mean? Confusing.</p><figure class="post-content__image-wrapper"><img class="post-content__image" src="/images/articles/logic-mixer.jpg" alt="The mixer section of Logic X"/><figcaption class="post-content__caption">The mixer section of Logic X (a typical DAW - Digital Audio Workstation)</figcaption></figure><p>For a long time I've been at least tangentially connected to the world of "pro" audio. As such, I'd feel uneasy when I overheard a lay-person talking about volume. I would often catch myself thinking "I know they are getting it wrong, but I don't know why". So after years of pretty much taking the decibel for granted, I finally had enough. I did some research, and it turns out the decibel is actually pretty interesting.</p><ul><li>A decibel is a relative unit. That means it expresses a ratio, and not an explicit value.</li><li>Decibels are measured using a logarithmic scale<sup id="fnref-2"><a href="#fn-2" class="footnote-ref">2</a></sup>.</li><li>They are a good unit for expressing very small <em>and</em> very large differences, and can do both on the same scale. This works because they are relative and logarithmic.</li><li>Decibels are a technical measure of intensity and/or pressure. Humans don't hear all frequencies equally, so "loudness" is a subjective concept.</li><li>When decibels <em>are</em> used to express "loudness", (for instance, by journalists) they are used with a fixed point of reference. In this context, the unit we're using is dBSPL (decibels relative to sound pressure level).</li><li>dBSPL represent an absolute value of sound pressure. Just like 20 metres is the same length wherever you go, 20 dBSPL always represents the same level of sound pressure. To use a decibel this way, we need a fixed point of reference. The reference point for dBSPL is 20μPa (micropascals) - roughly the limit of sensitivity of the human ear.</li><li>Levels of >120 dBSPL will damage our ears, but our pain threshold is somewhere around 150 dBSPL. So be careful, you could do lasting damage without realizing.</li><li>If you want to reference "loudness" using decibels, then you need to apply a filter of some kind. This more accurately describes the idea of "volume" as humans understand it.</li><li>The most common filter is the "A scale" (dBA). This is a "band-pass" filter, which is less sensitive to very high and very low frequencies. It's simplistic, but "close-enough" to have become a standard.</li><li>A less-simplistic approximation of what our ears hear would be a "hearing response curve". These are much more complex than the "A" scale, with peaks and troughs at different frequencies.</li></ul><figure class="post-content__image-wrapper"><figcaption class="post-content__caption">The graph on the left shows an "A" scale frequency response curve, while the graph on the right shows one that more closely matches the response of an average human ear.</figcaption></figure><p>Now that I've done my research, the dB markings on a mixing desk look far more logical. I can see now that using a simple low-to-high dB meter wouldn't make any sense. Any dB scale <em>requires</em> a defined reference point, which doesn't exist in this context. A mixer-channel <em>cannot</em> have a fixed frame of reference because it gets mixed. A mixer routes all channels to the "master bus" output channel, which has its own volume control. Knowing the value of a signal at the channel-level tells us nothing about the final output value.</p><p>But if we flip things around, a channel strip <em>can</em> have a fixed reference point. That fixed point is the maximum level that it can output. Because decibels are <em>relative</em> units, we can use them to express how far below the max-level a signal is. By putting 0dB at the top of the meter (and negative values on the downward scale) we can gain useful information about the level of the signal.</p><p>There is a technical term for this kind of decibel-usage - dBFS (decibel full scale). The highest possible level of sound produced by audio equipment is 0 dBFS. All other levels are then expressed in negative numbers. Absolute silence in dBFS is -∞ (minus infinity), but in practical terms -96 makes for a useable bottom point.</p><h2 id="putting-dbfs-to-use-on-the-web">Putting dBFS to use on the web.</h2><p>Much like a mixing desk, the Web Audio API does not use dBSPL. The final volume depends on both the level set by your operating system and the level of your speakers. Your computer has no way of knowing what your audio-out connects to (in an analogue setting, at least). It could be going to a set of tiny tinny earbuds, or a 500w amplifier and massive speaker stack. Providing a dBSPL reading of the level would be both meaningless and impossible. For this reason the Web Audio API uses dBFS.</p><p>In this scenario the dBFS value <em>is</em> useful information. It lets us know if a channel is "clipping", and gives us the information we need to keep on top of our gain-staging. It also gives us a value to compare against other channels on the same system. We may not know the volume of the final destination, but we can compare the individual channels. If we look at a drum track and a guitar track, for instance, we can use the dBFS values to guide how we mix the levels of the two.</p><h2 id="update-29th-march-2017"><em>Update, 29<sup>th</sup> March 2017:</em></h2><p>After it was first published this post made it onto the homepage of Hacker News, where it generated an interesting comment thread. I'm grateful to the HN community for pointing out a couple of typos and omissions, as well as raising some interesting points of their own.</p><ul><li>By nature of my specialties and interests, this post is very web-centric, which means it deals almost exclusively with <em>digital</em> audio. Once we make the transition into the analogue world, we start to see positive values for dBFS. This is known as "headroom", and accounts for the fact that analogue signals are commonly specified in dBV or dBu (0 references of 1 volt and ~0.775 volts, respectively). “So 0dBFS signal going into your USB audio interface might come out as a +16dBu analog signal, while the same digital signal going into a professional mixing console might come out as a +24dBu analog signal.” <a href="https://news.ycombinator.com/user?id=korethr">korethr</a></li><li>The medium through which sound (pressure) travels has an effect on how we define decibels. “In air, dB SPL is referenced to 20 micropascals. In water, it's 1 micropascal, so the medium matters.” <a href="https://news.ycombinator.com/user?id=hprotagonist">hprotagonist</a></li><li>Decibels are not just used in an audio context. I've now heard tales of them being used in spectrum analysis and anywhere that "pressure" needs to be measured. “Decibels are just a way to easily express ratios on a logarithmic scale. It's handy for all sorts of things.” <a href="https://news.ycombinator.com/user?id=pitaj">pitaj</a></li><li>Bonus points go to <a href="https://news.ycombinator.com/user?id=AndrewKemendo">AndrewKemendo</a> for opening my eyes to the bonkers world of "Car Audio SPL Drag Racing". Apparently some car owners compete to see which car-stereo can reach the highest dBSPL level when playing a single frequency. “It was instructive to compare frequency and spl because some cars resonant frequencies (fs) were better suited to different fs values of drivers (speakers) and then further down the line certain amplifiers optimized certain frequencies”. Just bananas!</li></ul><p>If you want to learn more about these interesting decibel side-stories, <a href="https://news.ycombinator.com/item?id=13931512">the comment thread for this post on HN</a> makes a good jumping-off point.</p><div class="footnotes"><hr/><ol><li id="fn-1">For instance, this <a href="https://en.wikipedia.org/wiki/Loudest_band_in_the_world">from Wikipedia</a>: The heavy metal band Manowar is one claimant of the title of "loudest band in the world", citing a measurement of 129.5 dB in 1994 in Hanover.<a href="#fnref-1" class="footnote-backref">↩</a></li><li id="fn-2">For a deeper dive into the science, check out <a href="http://www.animations.physics.unsw.edu.au/jw/dB.htm">this fantastically thorough explanation</a> from Australia's UNSW.<a href="#fnref-2" class="footnote-backref">↩</a></li></ol></div>Inline SVG icon sprites are (still) not scary.2017-02-26T00:00:00Zhttps://tomhazledine.com/inline-svg-icon-sprites/<p>You can see SVG icon sprites in use across the high-performance web. <a href="http://www.lonelyplanet.com/">Lonely Planet</a><sup id="fnref-1"><a href="#fn-1" class="footnote-ref">1</a></sup>, <a href="https://github.com">GitHub</a>, & <a href="http://codepen.io">CodePen</a> all use an SVG icon sprite, and there are <a href="https://sarasoueidan.com/blog/icon-fonts-to-svg/">plenty</a> <a href="https://cloudfour.com/thinks/seriously-dont-use-icon-fonts/">of</a> <a href="https://css-tricks.com/svg-sprites-use-better-icon-fonts/">advocates</a> for SVG icons across the development community. These examples come from the cutting-edge of front-end development, but SVG icons are not "hard" or "complicated". In fact, using an SVG sprite is one of the easiest ways to maintain an icon system.</p><p>Vector-based images are much better than raster formats (JPG & PNG) for creating icon systems for websites. They're easy to style with CSS, and have small file-sizes. The old way of dealing with this was to use <em>icon fonts</em> (where the icons were glyphs in a typeface), but a couple of years ago <a href="https://css-tricks.com/icon-fonts-vs-svg/">a better method emerged</a>: SVG icon sprites.</p><h2 id="using-an-svg-sprite">Using an SVG sprite</h2><p>We <em>could</em> add SVG icons to our sites using standard image-tag syntax – <code><img src="our_icon.svg"></code> – but that's not the best option. Every new <code><img></code> we add to the page creates a new HTTP request, which is bad for performance. The more requests you make, the longer the site takes to load<sup id="fnref-2"><a href="#fn-2" class="footnote-ref">2</a></sup>. As in the bad-old-days of image-sprites, it makes sense to group all our icons into a single request. Or even better: no extra requests at all. We can do this with SVG icon sprites.</p><p>We call them "sprites", but <em>SVG</em> sprites are much more elegant and programmatic than their rasterized forebears. The SVG language allows us to group blocks of icon-code in a single file for individual use later. All our disparate icon files get smushed into one big SVG, which we load like an asset. Then whenever we want to display a particular icon, we reference the relevant part of the master SVG file. No matter how many icons we use, or how often we use them, we're only ever including the complex SVG code once.</p><p>The reason we can do this is because SVG is a XML-like language. We can mark out different elements using tags in much the same manner as we'd mark-up our HTML. Instead of using <code><h1></code> tags for headings and <code><a></code> tags for links, we use tags for lines and shapes. We can also use tags to group our shapes together. The <code><g></code> tag ("g" == "group") is the obvious choice, but the <code><symbol></code> tag work best for our purposes<sup id="fnref-3"><a href="#fn-3" class="footnote-ref">3</a></sup>. The following code-snippet shows two separate icons within one SVG file:</p><pre><code class="hljs language-xml"><span class="hljs-tag"><<span class="hljs-name">svg</span> <span class="hljs-attr">xmlns</span>=<span class="hljs-string">"http://www.w3.org/2000/svg"</span>></span>
<span class="hljs-tag"><<span class="hljs-name">symbol</span> <span class="hljs-attr">viewBox</span>=<span class="hljs-string">"0 0 100 100"</span> <span class="hljs-attr">id</span>=<span class="hljs-string">"square_icon"</span>></span>
<span class="hljs-tag"><<span class="hljs-name">path</span> <span class="hljs-attr">d</span>=<span class="hljs-string">"M10 10H90V90H10Z"</span>/></span>
<span class="hljs-tag"></<span class="hljs-name">symbol</span>></span>
<span class="hljs-tag"><<span class="hljs-name">symbol</span> <span class="hljs-attr">viewBox</span>=<span class="hljs-string">"0 0 100 100"</span> <span class="hljs-attr">id</span>=<span class="hljs-string">"circle_icon"</span>></span>
<span class="hljs-tag"><<span class="hljs-name">circle</span> <span class="hljs-attr">cx</span>=<span class="hljs-string">"50"</span> <span class="hljs-attr">cy</span>=<span class="hljs-string">"50"</span> <span class="hljs-attr">r</span>=<span class="hljs-string">"40"</span>/></span>
<span class="hljs-tag"></<span class="hljs-name">symbol</span>></span>
<span class="hljs-tag"></<span class="hljs-name">svg</span>></span>
</code></pre><p>Notice how we've added a standard HTML <code>id</code> attribute to each <code><symbol></code>. This is the "secret sauce" that makes SVG sprites useful. We can now load our entire SVG at the top of our page and extract the symbols whenever we need them.</p><p>With the sprite-code in place, using individual icons becomes much simpler. We can include them whenever and wherever we need them by taking advantage of SVG's <code><use></code> tag.</p><pre><code class="hljs language-xml"><span class="hljs-tag"><<span class="hljs-name">svg</span>></span>
<span class="hljs-tag"><<span class="hljs-name">use</span> <span class="hljs-attr">xlink:href</span>=<span class="hljs-string">"#square_icon"</span>/></span>
<span class="hljs-tag"></<span class="hljs-name">svg</span>></span>
</code></pre><p>And of course we can still add classes to the <code><svg></code> tags, allowing us to style an icon in as many different ways as we like.</p><pre><code class="hljs language-html"><span class="hljs-tag"><<span class="hljs-name">ul</span> <span class="hljs-attr">class</span>=<span class="hljs-string">"example_svg_list"</span>></span>
<span class="hljs-tag"><<span class="hljs-name">li</span>></span>
<span class="hljs-tag"><<span class="hljs-name">svg</span> <span class="hljs-attr">class</span>=<span class="hljs-string">"example_svg_1"</span>></span><span class="hljs-tag"><<span class="hljs-name">use</span> <span class="hljs-attr">xlink:href</span>=<span class="hljs-string">"#example_svg"</span> /></span><span class="hljs-tag"></<span class="hljs-name">svg</span>></span>
<span class="hljs-tag"></<span class="hljs-name">li</span>></span>
<span class="hljs-tag"><<span class="hljs-name">li</span>></span>
<span class="hljs-tag"><<span class="hljs-name">svg</span> <span class="hljs-attr">class</span>=<span class="hljs-string">"example_svg_2"</span>></span><span class="hljs-tag"><<span class="hljs-name">use</span> <span class="hljs-attr">xlink:href</span>=<span class="hljs-string">"#example_svg"</span> /></span><span class="hljs-tag"></<span class="hljs-name">svg</span>></span>
<span class="hljs-tag"></<span class="hljs-name">li</span>></span>
<span class="hljs-tag"><<span class="hljs-name">li</span>></span>
<span class="hljs-tag"><<span class="hljs-name">svg</span> <span class="hljs-attr">class</span>=<span class="hljs-string">"example_svg_3"</span>></span><span class="hljs-tag"><<span class="hljs-name">use</span> <span class="hljs-attr">xlink:href</span>=<span class="hljs-string">"#example_svg"</span> /></span><span class="hljs-tag"></<span class="hljs-name">svg</span>></span>
<span class="hljs-tag"></<span class="hljs-name">li</span>></span>
<span class="hljs-tag"></<span class="hljs-name">ul</span>></span>
</code></pre><ul class="example_svg_list cluster--center"><li/><li/><li/></ul><h2 id="building-an-svg-sprite-with-gulp">Building an SVG sprite with Gulp</h2><p>Hand-coding SVG icon sprites can be a real chore. It gives your site's <em>users</em> the best experience possible, but it's not fun for developers. Even combining <em>simple</em> SVGs into <code><symbol></code> elements inside a single <code><svg></code> is not a fun way to spend your time. And real-world SVGs are never as easy to read as the examples show here. What we need is a way to automate the sprite-creation process.</p><p>I automate other front-end tasks using a <em>task runner</em>. If you've made it this far into an article about creating SVG icon sprites, then I'm going to assume you use one too. Gulp is my task-runner of choice, but you might be using something like Grunt<sup id="fnref-4"><a href="#fn-4" class="footnote-ref">4</a></sup>. Gulp already handles my javascript and css, so adding my icons into the mix is a natural progression.</p><p>Adding an SVG icon sprite to my Gulp setup was quick & painless, and the same task has been serving me well for over two years now. The <a href="https://github.com/jkphl/gulp-svg-sprite"><code>gulp-svg-sprite</code> module</a> runs error-free without much need for configuration. If you've already got a <code>gulpfile.js</code> in your project, you can install the module like this:</p><pre><code class="hljs language-bash">npm install gulp-svg-sprite --save
</code></pre><p>That module takes individual SVG files and combines them into a single <code><svg></code>. Each file becomes a <code><symbol></code> with an ID matching the original filename.</p><p>So a folder structure like this:</p><pre><code class="hljs language-bash">SVG_folder
- icon_one.svg
- second_icon.svg
</code></pre><p>Becomes a sprite like this:</p><pre><code class="hljs language-xml"><span class="hljs-tag"><<span class="hljs-name">svg</span> <span class="hljs-attr">xmlns</span>=<span class="hljs-string">"http://www.w3.org/2000/svg"</span>></span>
<span class="hljs-tag"><<span class="hljs-name">symbol</span> <span class="hljs-attr">id</span>=<span class="hljs-string">"icon_one"</span>></span>
<span class="hljs-tag"><<span class="hljs-name">path</span> <span class="hljs-attr">d</span>=<span class="hljs-string">"/* path data */"</span>/></span>
<span class="hljs-tag"></<span class="hljs-name">symbol</span>></span>
<span class="hljs-tag"><<span class="hljs-name">symbol</span> <span class="hljs-attr">id</span>=<span class="hljs-string">"icon_two"</span>></span>
<span class="hljs-tag"><<span class="hljs-name">path</span> <span class="hljs-attr">d</span>=<span class="hljs-string">"/* path data */"</span>/></span>
<span class="hljs-tag"></<span class="hljs-name">symbol</span>></span>
<span class="hljs-tag"></<span class="hljs-name">svg</span>></span>
</code></pre><p>Once we've installed the module, we need to build our task. The important things to set are the source folder and destination folder. I.e. where the task will <em>find</em> the raw SVGs, and where it will <em>put</em> the sprite.</p><pre><code class="hljs language-js"><span class="hljs-comment">// Load the module.</span>
<span class="hljs-keyword">var</span> svgSprite = <span class="hljs-built_in">require</span>(<span class="hljs-string">"gulp-svg-sprite"</span>);
<span class="hljs-comment">// Set our desired configuration values.</span>
svgConfig = {
<span class="hljs-attr">mode</span>: {
<span class="hljs-comment">// Make sure we're combining icons</span>
<span class="hljs-comment">// using the <symbol> method.</span>
<span class="hljs-attr">symbol</span>: <span class="hljs-literal">true</span>
},
<span class="hljs-comment">// Some more settings to keep</span>
<span class="hljs-comment">// the SVG's code clean:</span>
<span class="hljs-attr">svg</span>: {
<span class="hljs-attr">xmlDeclaration</span>: <span class="hljs-literal">false</span>,
<span class="hljs-attr">doctypeDeclaration</span>: <span class="hljs-literal">false</span>,
<span class="hljs-comment">// By default the module wants to namespace</span>
<span class="hljs-comment">// all our IDs and classes. We're grownups</span>
<span class="hljs-comment">// so we want to preserve our settings.</span>
<span class="hljs-attr">namespaceIDs</span>: <span class="hljs-literal">false</span>,
<span class="hljs-attr">namespaceClassnames</span>: <span class="hljs-literal">false</span>
}
};
<span class="hljs-comment">// Define our task.</span>
gulp.<span class="hljs-title function_">task</span>(<span class="hljs-string">"svg"</span>, <span class="hljs-keyword">function</span> (<span class="hljs-params"/>) {
<span class="hljs-comment">// Set the source folder.</span>
gulp.<span class="hljs-title function_">src</span>(<span class="hljs-string">"uncompressed/icons/**/*.svg"</span>)
<span class="hljs-comment">// Include our options.</span>
.<span class="hljs-title function_">pipe</span>(<span class="hljs-title function_">svgSprite</span>(svgConfig))
<span class="hljs-comment">// Set the destination folder.</span>
.<span class="hljs-title function_">pipe</span>(gulp.<span class="hljs-title function_">dest</span>(<span class="hljs-string">"assets/icons"</span>));
});
</code></pre><p>With this task in place, maintaining an SVG icon sprite is easy. Whenever we add a new icon to our source folder we just need to run <code>gulp svg</code>. That takes our raw files, cleans them up, and combines them into a single file.</p><h2 id="keeping-the-intermediate-files">Keeping the intermediate files.</h2><p>When creating our sprite, the module first cleans-up our SVGs. It removes empty tags, strips out any unnecessary attributes, and minifies the files. This can come in handy if you need easy access to nice, neat SVG code for an individual icon.</p><p>If you want to target discrete elements <em>within</em> an icon, you may need to include the raw markup in your page. This can be necessary when you want to build complex animations or transitions. Being able to copy/paste the nicely-configured code is useful in these situations.</p><p>You can configure the module to create a folder with the individual SVGs in after they've been optimized and cleaned-up by the module. You can do this by adding the following rule to the <code>svgConfig</code> object:</p><pre><code class="hljs language-js"><span class="hljs-attr">shape</span>: {
<span class="hljs-comment">// Choose a folder to store the</span>
<span class="hljs-comment">// intermediate SVG files in.</span>
<span class="hljs-attr">dest</span>: <span class="hljs-string">"intermediate"</span>;
}
</code></pre><h2 id="including-the-sprite-in-a-site">Including the sprite in a site</h2><p>Once Gulp has created the sprite file for us, we need to add that to our page below your opening <code><body></code> tag. Copy-pasting the contents of <code>sprite.symbol.svg</code> would do the trick. But we can automate this process by including a reference to our sprite file.</p><dl><dt>Including an SVG icon sprite in a Jekyll site</dt><dd>In the past I've worked a lot with Jekyll - a static-site generator. When it comes to performance and security it's hard to beat static HTML files. Jekyll compiles a site <em>before</em> it gets put on a server. There are no databases to hack, and no server-side build-steps to slow the site down. Because Jekyll is already combining lots of files together, adding a sprite into the mix is easy.</dd></dl><p>We can get Gulp to output the sprite into Jekyll's <code>_includes</code> directory by changing the path in <code>gulp.dest()</code>. So <code>.pipe( gulp.dest( 'assets/icons' ) );</code> becomes <code>.pipe( gulp.dest( '_includes' ) );</code>. Then you can pull in your sprite with a simple "include" statement:</p><pre><code class="hljs language-liquid">{% include /symbol/svg/sprite.symbol.svg %}
</code></pre><p>Remember to use Liquid's <code>{% raw %}</code> tag to ensure the SVG code doesn't get escaped.</p><dl><dt>Including an SVG icon sprite in a WordPress site</dt><dd>I built this site in WordPress, which has its own conventions for including files. When I want to inject code into the markup, `get_template_part()` is the preferred method. The only downside is that `get_template_part()` only works for PHP files.</dd></dl><p>I get around this obstacle with the following Gulp task:</p><pre><code class="hljs language-js">gulp.<span class="hljs-title function_">task</span>(<span class="hljs-string">"svg_rename"</span>, <span class="hljs-keyword">function</span> (<span class="hljs-params"/>) {
<span class="hljs-keyword">return</span> gulp
.<span class="hljs-title function_">src</span>(<span class="hljs-string">"assets/icons/symbol/svg/*.svg"</span>)
.<span class="hljs-title function_">pipe</span>(<span class="hljs-title function_">rename</span>(<span class="hljs-string">"iconsprite.svg.php"</span>))
.<span class="hljs-title function_">pipe</span>(gulp.<span class="hljs-title function_">dest</span>(<span class="hljs-string">"assets/icons"</span>));
});
</code></pre><p>That task renames the sprite and saves the renamed file in the <code>assets/icons</code> directory. Note that we're adding <em>two</em> file extensions: <code>.svg</code> and <code>.php</code>. Once we've generated this file we include it in our code like this:</p><pre><code class="hljs language-php"><span class="hljs-meta"><?php</span> <span class="hljs-title function_ invoke__">get_template_part</span>(<span class="hljs-string">'assets/icons/iconsprite.svg'</span>); <span class="hljs-meta">?></span>
</code></pre><dl><dt>Hiding the sprite</dt><dd>When we load a page with an SVG icon sprite in it, the browser will try to render the sprite code. This can create a large empty space at the top of our page. The only time we want our SVG code to appear is when we reference it with a <code><use/></code> tag, so we want to hide the raw sprite. Remember to add a style of `display:none;` to the sprite or it's container.</dd></dl><hr/><p>Building and using SVG icon sprites sounds complicated, and it is. But maintaining a performant and scalable icon system is a non-trivial task. That complexity has to live <em>somewhere</em>, and icon sprites are by far the best of the options<sup id="fnref-5"><a href="#fn-5" class="footnote-ref">5</a></sup>. Couple the sprite with a sensible workflow and the whole process becomes second-nature.</p><p>I've been using <a href="http://caniuse.com/#search=SVG">SVG icon sprites in production</a> for over two years now. I have yet to see a simpler solution. Now that I've got by Gulp setup how I like it, I can be confident that all the tools I need are at my fingertips. Whenever I need to add a new icon to a site, it's a two-step process. I save an SVG to my <code>uncompressed/icons</code> folder and drop a corresponding <code><use></code> element in my markup.</p><p>Like many good ideas before them, SVG icon sprites are spreading across the web. And like Tyler Sticka said: <a href="https://cloudfour.com/thinks/seriously-dont-use-icon-fonts/">Don't be Table Guy</a>.</p><div class="footnotes"><hr/><ol><li id="fn-1">Ian Feather wrote a great article about when <a href="http://ianfeather.co.uk/ten-reasons-we-switched-from-an-icon-font-to-svg/">Lonely Planet switched from using an icon-font to SVG icon sprites</a>.<a href="#fnref-1" class="footnote-backref">↩</a></li><li id="fn-2">The number of requests is a big deal with regular HTTP connection. HTTP-2, however, changes things; but you'll have to find out more about that elsewhere.<a href="#fnref-2" class="footnote-backref">↩</a></li><li id="fn-3">We could just use a <code><g></code> tag – or even reference individual paths and shapes directly – but <code><symbol></code> allows us to add a <code>viewBox</code> attribute. This means we only have to declare the view-box once for each icon, rather than repeating ourselves every time we use the icon in our page.<a href="#fnref-3" class="footnote-backref">↩</a></li><li id="fn-4">Note that the raw <a href="https://github.com/jkphl/svg-sprite"><code>svg-sprite</code> Node module</a> doesn't read or write to the file system, so you'll need to use <em>something</em> to help you out.<a href="#fnref-4" class="footnote-backref">↩</a></li><li id="fn-5">It is worth looking at the counter argument in favour of icon fonts, because SVG sprites may not be appropriate for everybody. Ben Frain probably <a href="https://benfrain.com/seriously-use-icon-fonts/">makes the best case against SVG sprites</a>, but his argument is about <em>changing</em> from fonts to sprites. Kind of a "if it ain't broke" thesis.<a href="#fnref-5" class="footnote-backref">↩</a></li></ol></div>Getting to grips with SVG markup2017-02-15T00:00:00Zhttps://tomhazledine.com/svg-markup/<p>Here is a side-by-side comparison of an icon. The image on the left is an SVG, the one on the right is a raster PNG. They are both roughly the same file size (~1Kb), and at 64px x 64px they look pretty similar. But see how those same files look at double-scale:</p>
<div class="svg-vs-png">
<span class="hidden--visually">*If you're reading this post through some kind of syndication feed (RSS, etc.) you may need to visit <a href="https://tomhazledine.com/svg-markup/">the original post</a> to view this image-demo correctly.*</span><div class="one-x clearfix">
<div class="item-label">
<p>An .SVG and .PNG icon, side-by-side</p>
</div>
<div class="item-wrapper">
<div class="item-mask">
</div>
</div>
<div class="item-wrapper">
<div class="item-mask">
<img class="masked-image" src="/images/articles/code_128.png"/>
</div>
</div>
</div><div class="two-x clearfix">
<div class="item-label">
<p>The same files, increased to double-size</p>
</div>
<div class="item-wrapper">
<div class="item-mask">
</div>
</div>
<div class="item-wrapper">
<div class="item-mask">
<img class="masked-image" src="/images/articles/code_128.png"/>
</div>
</div>
</div><div class="ten-x clearfix">
<div class="item-label">
<p>...and to 10x the original size</p>
</div>
<div class="item-wrapper">
<div class="item-mask">
</div>
</div>
<div class="item-wrapper">
<div class="item-mask">
<img class="masked-image" src="/images/articles/code_128.png"/>
</div>
</div>
</div>
</div>
<p>We can use the same SVG file for all sizes, and produce consistently clear and crisp results. To get the same effect with rasterized images, we'd need to create separate files for each size. Each increase in visible size would result in an increase of file size, too. We'd not only be pushing up the number of HTTP requests but also the total page-weight as well.</p>
<h2 id="the-basics-of-the-svg-markup-language" tabindex="-1">The basics of the SVG markup language</h2>
<p>SVG as a format needn't be scary or intimidating. SVG stands for Scalable Vector Graphic, and the language of SVGs is an XML-derived language like RSS feeds and XHTML<sup class="footnote-ref"><a href="#fn1" id="fnref1">[1]</a></sup>. Once you know what tags to look out for, the whole thing gets a lot simpler. Take this simplified SVG markup, for example:</p>
<pre><code class="language-xml"><svg viewBox="0 0 100 100">
<path d="M10 10H90V90H10Z"/>
<circle cx="50" cy="50" r="40"/>
</svg>
</code></pre>
<p>Even without being able to read the more technical aspects, there are clues to pick up on. It's clear that the SVG above contains a <em>path</em> (a.k.a. a <em>line</em>) and a <em>circle</em>. Dive a little deeper and you'll see that it's a circle inside a square. The circle has a diameter of 80 ("r" == "radius"), and sits inside a 90 x 90 square (the line joins up with itself to form a <em>shape</em>). Both of these lie in the centre of a 100 x 100 artboard (a.k.a "view box").</p>
<svg class="inline-image-block" style="enable-background:new 0 0 100 100;" width="200px" height="200px" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" version="1.1" id="Layer_1" x="0px" y="0px" viewBox="0 0 100 100" xml:space="preserve">
<style type="text/css">
.svg_demo_1_a{fill:#E3E1E1;}
.svg_demo_1_b{fill:none;stroke:#000000;stroke-linecap:round;stroke-linejoin:round;stroke-miterlimit:10;stroke-dasharray:0,4;}
</style>
<rect class="svg_demo_1_a" width="100" height="100"/>
<g>
<g>
<polyline class="svg_demo_1_b" points="95,94.5 95,95 94.5,95 "/>
<line class="svg_demo_1_b" x1="93.5" y1="95" x2="6" y2="95"/>
<polyline class="svg_demo_1_b" points="5.5,95 5,95 5,94.5 "/>
<line class="svg_demo_1_b" x1="5" y1="93.5" x2="5" y2="6"/>
<polyline class="svg_demo_1_b" points="5,5.5 5,5 5.5,5 "/>
<line class="svg_demo_1_b" x1="6.5" y1="5" x2="94" y2="5"/>
<polyline class="svg_demo_1_b" points="94.5,5 95,5 95,5.5 "/>
<line class="svg_demo_1_b" x1="95" y1="6.5" x2="95" y2="94"/>
</g>
</g>
<g>
<circle class="svg_demo_1_b" cx="50" cy="50" r="45"/>
</g>
</svg>
<p>Real-world SVGs are more complex, of course. There are plenty of attributes and DOCTYPE declarations to worry about. But the general principle is the same. There is an opening tag (<code><svg></code>) and closing tag (<code></svg></code>) just like <code><html></code>. And we build shapes within the SVG using tags like <code><path/></code> and <code><circle/></code> and <code><rect/></code> (rectangle)<sup class="footnote-ref"><a href="#fn2" id="fnref2">[2]</a></sup>.</p>
<h2 id="styling-svgs-with-css" tabindex="-1">Styling SVGs with CSS</h2>
<p>SVGs are complex, for sure, but that very complexity gives them their power. And we don't need to know the intricacies of the co-ordinate system to harness that power. All we need to do is recognize the different elements. This is a language much like HTML, after all, so once we can find the different shapes in an icon we can add classes to them.</p>
<pre><code class="language-xml"><svg viewBox="0 0 100 100">
<path class="outer_square" d="M10 10H90V90H10Z"/>
<circle class="inner_circle" cx="50" cy="50" r="40"/>
</svg>
</code></pre>
<p>...which we can then style with CSS:</p>
<pre><code class="language-css">svg {
background: yellow;
}
.outer_square {
stroke: red;
stroke-width: 1px;
}
.inner_circle {
fill: blue;
}
</code></pre>
<svg class="inline-image-block" style="enable-background:new 0 0 100 100;" width="200px" height="200px" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" version="1.1" id="Layer_1" x="0px" y="0px" viewBox="0 0 100 100" xml:space="preserve">
<style type="text/css">
.svg_demo_2_a{fill:#FCEE21;}
.svg_demo_2_b{fill:none;stroke:#FF0000;stroke-miterlimit:10;}
.svg_demo_2_c{fill:#0000FF;}
</style>
<rect class="svg_demo_2_a" width="100" height="100"/>
<rect x="5" y="5" class="svg_demo_2_b" width="90" height="90"/>
<circle class="svg_demo_2_c" cx="50" cy="50" r="45"/>
</svg>
<p>This gives us a more fine-grained level of control than a single character in an icon font would. We can manipulate the styles of each discrete part of an icon with CSS. Transitions and animations provide an extra level of detail. Our positioning can be much more precise. We have a defined and predictable co-ordinate system to work with. No more fiddling about with baseline alignment of invisible-hitboxed font characters!</p>
<p>There are some fantastically clever and artistic examples of SVG in the wild. The only limits appear to be the creativity and imagination of the developers who build them. A browse through CodePen reveals stunning work, but I'll end with a more simple demonstration of my work.</p>
<p>I've always had a soft-spot for loading graphics, and love to build them myself. I built this one with a two-path SVG and a dash of CSS animation:</p>
<p data-height="265" data-theme-id="0" data-slug-hash="BKrXwJ" data-default-tab="result" data-user="tomhazledine" data-embed-version="2" data-pen-title="Hourglass Loader" class="codepen">See the Pen <a href="http://codepen.io/tomhazledine/pen/BKrXwJ/">Hourglass Loader</a> by Tom Hazledine (<a href="http://codepen.io/tomhazledine">@tomhazledine</a>) on <a href="http://codepen.io">CodePen</a>.</p>
<script async="async" src="https://production-assets.codepen.io/assets/embed/ei.js"/>
<hr class="footnotes-sep"/>
<section class="footnotes">
<ol class="footnotes-list">
<li id="fn1" class="footnote-item"><p>This is not the place for a debate about the nuances of XHTML vs HTML vs SGML. If that kind of language-geekery is for you, you can <a href="https://www.youtube.com/watch?v=RH0o-QjnwDg">learn more about the *ML "family" from Computerphile</a>. <a href="#fnref1" class="footnote-backref">↩︎</a></p>
</li>
<li id="fn2" class="footnote-item"><p>You can read more about the inner-workings of SVGs - including a detailed look at how to construct paths using co-ordinates – on the <a href="https://developer.mozilla.org/en-US/docs/Web/SVG/Tutorial/Paths">MDN SVG tutorial pages</a>. <a href="#fnref2" class="footnote-backref">↩︎</a></p>
</li>
</ol>
</section>Bullet Journal Revisited2017-01-20T00:00:00Zhttps://tomhazledine.com/bullet-journal-revisited/<figure class="post-content__image-wrapper post-content__image-wrapper--has-sidebar">
<img class="post-content__image" src="/images/articles/journal.jpg" alt=""/>
<figcaption class="sidenote">My original analogue attempt at maintaining a Bullet Journal</figcaption>
</figure>
<p>The "Bullet Journal"<sup class="footnote-ref"><a href="#fn1" id="fnref1">[1]</a></sup> is a system for <em>Getting Things Done</em>. One nested pen-and-paper list that gets rewritten every morning. My first post on this site was about <a href="https://tomhazledine.com/bullet-journal-workflow">the success I was having with the Bullet Journal</a>. Over the last few weeks I've begun to use the journal system again, so I figured it was time for an update.</p>
<p>I used the Bullet Journal daily for about ten months (covering most of my time as a freelance developer). When an agency recruited me, I stopped using it. They had their own task-management tools, and I no longer had such an urgent need to organize my own time.</p>
<p>Fast-forward several years and I was beginning to feel the lack of a personal organization system. I was getting stuff done in work hours, for sure, but momentum was slipping on all my "side projects". I don't get people who struggle to come up with ideas for projects: I'm overwhelmed by ideas. What I find hard is <em>execution</em>. Getting Things Done is my Achilles heel. It was time to revisit the Bullet Journal.</p>
<h2 id="going-digital." tabindex="-1">Going digital.</h2>
<p>When I kept the daily task-list, I had a dedicated paper notebook. Keeping things analogue (as per the "pure" Bullet Journal method) was a way of maintaining focus. But now I'm much more comfortable keeping everything in the "cloud". I keep all my notes in a private Dropbox folder so I can access them wherever I am, regardless of what device I'm using. It also allows me to use my favourite writing apps<sup class="footnote-ref"><a href="#fn2" id="fnref2">[2]</a></sup>.</p>
<p>Constant access to my files has removed the downsides of keeping a digital list. In 2013 I was more comfortable carrying a physical notebook around with me. Now, I find it more convenient to keep a folder full of markdown files in the cloud.</p>
<p>Because I spend so much time working with markdown documents, and because markdown has such an elegant way of handling bulleted lists (with checkboxes!), adapting the Bullet Journal method was a natural choice.</p>
<h2 id="my-evolved-bullet-journal-system." tabindex="-1">My evolved Bullet Journal system.</h2>
<p>The central tenets of my journal are the same as the "official" system. I have one task list, and that gets refreshed every morning. I remove completed items, and anything still outstanding gets re-evaluated<sup class="footnote-ref"><a href="#fn3" id="fnref3">[3]</a></sup>.</p>
<p>Because the list is digital, I don't need to worry about adding a "migrated" marker when a task rolls-over into the next day. If it's done, it gets deleted. If I want it to "migrate" to the next day's list, it stays where it is. I've also moved away from the other markers used in the original setup. Sticking to the nested list works nicely for me, and I found the extra markers for events etc. made my lists over-complicated and overwhelming.</p>
<p>I nest my list by topic. By way of example, here's a snippet of a list I made for <a href="https://tomhazledine.com/calculating-reading-speed">adding a new feature</a> to this very site:</p>
<pre><code class="language-markdown">- [ ] My personal website // This is the "master" list item
- [ ] readtime calculation
_ [x] calculate seconds based on words-per-minute // I have completed this item.
_ [ ] parse time into human-readable string \* [ ] write a blog post about the process + [ ] another feature
// ...
</code></pre>
<p>When I kept my analogue journal, I would create "super objectives" for every month. The idea was that I could refer to those pages and make sure I was keeping up with my "big picture" goals. This was always a cumbersome process, and these super-objectives were often overlooked.</p>
<p>Space is not a factor for my digital journal, so I can afford to add as many items as I like. I even break those items into groups if the mood takes me. Right now I have "Big goals", "Technologies to learn (and master?)", "Dream goals", and "Short-term tasks".</p>
<p><em>Big goals</em> is at top of the page and has all my serious objectives for the year ahead (this is a new addition for 2017 - it's early days yet, so we'll see how this pans out...). My hope is that by keeping these items front-and-center means I can't ignore them. I've discovered there are few things more disheartening than finding a year-old task list and not being able to tick-off a single item.</p>
<p><em>Technologies to learn (and master?)</em> helps me keep on top of the things I'm trying to learn. Sample entries at the moment include <code>vue.js</code> & <code>web audio API</code>.</p>
<p><em>Dream goals</em> is more whimsical. These are things I'd like to do, but am unlikely to find time for. But by writing them down I can justify a major change in tack if a great opportunity comes along. (“Sure I can blow some money on a <a href="http://shop.premium-coppers.com/product-category/premium-copper-riveted-union-alembic-stills/">fancy copper alembic-still</a>; <code>make some whisky</code> is on my task list!”). It also helps to keep my procrastination in-check. If I find myself wasting time on some shiny new idea, I can't use the old "well, I've always dreamt of doing this" excuse. If I cared enough about it, I'd have put it on the list!</p>
<p><em>Short-term tasks</em> is the most mundane section, but also the most used and the most useful section. Lately I've been focusing on improving this website. As a result, my list is full of items like <code>setup LetsEncrypt on server</code> and <code>filter out lang-spam in G. Analytics</code>. Most of the items in this section are getting checked off, which is a signal that I'm close to my goal<sup class="footnote-ref"><a href="#fn4" id="fnref4">[4]</a></sup>. Time to start thinking about the next short-term goal...</p>
<p>In the past, it has always been the time in-between projects when my focus has dipped. And once I lose my focus, I find it very hard to get back into the groove again. Predicting an imminent "danger zone" helps me sustain a higher level of productivity.</p>
<hr/>
<h2 id="update%3A" tabindex="-1">Update:</h2>
<p>Coincidentally, it appears I'm not the only one who's been investigating the Bullet Journal lately:</p>
<div>
<script type="text/javascript" src="https://ssl.gstatic.com/trends_nrtr/884_RC03/embed_loader.js"/>
<script type="text/javascript">trends.embed.renderExploreWidget("TIMESERIES", {"comparisonItem":[{"keyword":"bullet journal","geo":"","time":"today 5-y"}],"category":0,"property":""}, {"exploreQuery":"q=bullet%20journal","guestPath":"https://www.google.co.uk:443/trends/embed/"});</script>
</div>
<p>After this post was written, I checked Google's Search Trends tool and saw a surprising spike. My original 2013 article always sees a uplift in traffic around the new year, but the last twelve months have seen consistent growth that correlates with the general Google trend. My hope is that everyone who's trying out the Bullet Journal (myself included) can turn that interest into a genuine boost for productivity.</p>
<hr class="footnotes-sep"/>
<section class="footnotes">
<ol class="footnotes-list">
<li id="fn1" class="footnote-item"><p>Credit for the <a href="http://www.bulletjournal.com/">Bullet Journal</a> concept goes to digital product designer <a href="http://www.rydercarroll.com/">Ryder Carrol</a>. <a href="#fnref1" class="footnote-backref">↩︎</a></p>
</li>
<li id="fn2" class="footnote-item"><p>My favourite writing apps are <a href="https://www.sublimetext.com">Sublime Text</a> on my laptops and desktop, and <a href="https://bywordapp.com/">Byword</a> on phone and tablet. I do all my writing in Markdown (be it drafts for complicated emails, notes on work projects, or aide-mémoire). <a href="#fnref2" class="footnote-backref">↩︎</a></p>
</li>
<li id="fn3" class="footnote-item"><p>The daily re-evaluation of all uncompleted tasks keeps the list fresh. <a href="#fnref3" class="footnote-backref">↩︎</a></p>
</li>
<li id="fn4" class="footnote-item"><p>The "new year refresh" has passed the 80% complete mark. That's farther than I've ever got before. Time to move on to the next project! <a href="#fnref4" class="footnote-backref">↩︎</a></p>
</li>
</ol>
</section>Well-written HTML doesn't need any styling. Except that it does.2017-01-15T00:00:00Zhttps://tomhazledine.com/html-doesnt-need-any-styling/<p>One of the best things about HTML is that it just works. As with much of the web, things only get weird when designers and developers start adding things. By default, a raw HTML file will render quite nicely in every browser on every device. Hierarchy is clear, lists look like lists, tables look like tables, we have <em>bold</em> text and <em>italics</em>, and <a href="#">links</a> (the ‘hyper’ in HTML) are obvious.</p>
<p>Lately I've discovered a few bloggers who have embraced this quality. <a href="http://danluu.com/">Dan Luu</a>, for instance, is such a developers'-developer that he doesn't even have a stylesheet on his blog. All that matters is the content, free from artifice or sneaky designer-tricks. His writing stands or falls on its own merits. HTML has everything he needs, so why include anything else?</p>
<p>Myself, I disagree. Reading his articles is <em>hard</em>, even though they are expertly written. The aesthetic works well for him - it suits the style of his writing, and emphasises his credentials - but there's one aspect that makes his pages nigh-on unreadable on all but a few devices: the lines are simply too long.</p>
<p>My monitor at home is bigger than most, but not huge by modern standards. At this size, most paragraphs appear as just a couple of lines stretching the width of the screen. There's a reason books are the size and proportions that they are: there is an upper limit to the line-length that a person can comfortably read.</p>
<p>Counter to what you might expect, our eyes don't smoothly follow the lines on the page. When we read our eyes are actually jumping back and forth in very small movements<sup class="footnote-ref"><a href="#fn1" id="fnref1">[1]</a></sup>. If the lines get too long, our brain struggles to tell which line is which and the movements become unaligned: everything scrambles, and we have to work hard to find our place again. Below a certain length (usually somewhere between 55 and 75 characters) our eyes can easily discern where the next line begins.</p>
<p>The spacing between the lines also plays a part here (known as "leading" after the strips of lead placed between the lines of text in letterpress printing). Too wide or too tight a space, and our eyes struggle to get their bearings. The best books have just the right balance of leading and line-length. Raw HTML, however, is not at all bothered by the lengths of its lines. Super widescreen display? HTML will keep those lines running from edge to edge, readability be damned.</p>
<p>There is, of course, an easy fix. All we need to do is add three lines of CSS to our HTML page, and everything looks much better.</p>
<pre><code class="language-css">body {
max-width: 50em;
padding: 2em 0.5em;
margin: 0 auto;
}
</code></pre>
<p>Here we're setting a <code>max-width</code>: if we used a simple <code>width</code>, we'd force the page to be that wide even on small screens (so we'd have to scroll sideways to see all our content). This runs counter to the spirit of HTML, which fits to any available space, so using <code>max-width</code> is better (if the screen is larger than 50em, it will cap the line-length, otherwise it lets HTML do its thing).</p>
<p>Notice, too, that we're using <code>em</code> units. These are linked to the size of the type (literally the width of a letter "m"), so we can be sure the width will be proportional to the text.</p>
<p>As for the rest of the rules, the <code>padding</code> is making sure the text is never jammed uncomfortably close to the edges of the screen, and the <code>margin: 0 auto;</code> rule then centres the block in the middle of the screen.</p>
<p>That's not a lot of styling, but it has a tremendous impact on the readability of a web page.</p>
<p>One of the benefits of presenting your content in nothing but HTML is that the browser only has to make a single file request to the server. Traditional CSS lives in its own file, and is therefore another request. Normally that's no problem at all, but in this context it's literally doubling the amount of load the page places on the server. The simple get-out for this is to in-line the CSS rules within the HTML file. It's not stylistically "pure" or "best practice", but does actually work.</p>
<p>So next time you're swept up in the machismo of <a href="http://motherfuckingwebsite.com/">a HTML-purist rallying cry</a>, spare a thought for your readers and think about your line-length.</p>
<hr class="footnotes-sep"/>
<section class="footnotes">
<ol class="footnotes-list">
<li id="fn1" class="footnote-item"><p>The tiny eye movements are called <em>saccades</em>. Our brain is taking-in the whole block of text at once, not just the specific word we've got to. It's similar to how we perceive paintings: our eyes can only focus on one tiny part at any one time, but we "see" the picture as a whole. As an interesting aside, the size of the in-focus area you can see at any one time is easy to demonstrate. Hold your arm out in front of you with your thumb pointing up: the in-focus area is roughly the size of your thumbnail. Our brains fill in the rest while our eyes dart around faster than we can perceive. <a href="#fnref1" class="footnote-backref">↩︎</a></p>
</li>
</ol>
</section>n-minute read: calculating an average reading speed2017-01-05T00:00:00Zhttps://tomhazledine.com/calculating-reading-speed/<p>I'm interested in measuring what effect, if any, displaying an approximate reading-time for articles will have on readers of my site<sup class="footnote-ref"><a href="#fn1" id="fnref1">[1]</a></sup>. Will people be put off by knowing in advance that an article will take a long time to read? Will they gravitate to shorter pieces, or avoid them because they're looking for more in-depth content? Will it have no effect at all?</p>
<p>My hypothesis is this: adding a read-time to post summaries will increase click-throughs from the archive page to full-post views. It reduces the "mystery meat" problem by providing users with objective data about their options, meaning they are better prepared to make a decision about which post to read.</p>
<p>Because this is an increasingly common idea<sup class="footnote-ref"><a href="#fn2" id="fnref2">[2]</a></sup>, there are <a href="http://zurb.com/forrst/posts/Medium_like_estimated_reading_time_in_PHP-G6j">many</a> <a href="https://gist.github.com/mynameispj/3170442">examples</a> of reading-time-calculation functions out there. I've borrowed and adapted concepts from all over the place, but diverged from most I saw in two main ways.</p>
<ul>
<li>Firstly, where others have focused on concision, I have opted for a more verbose style. One of my goals in writing pieces like this is to fully understand the code I'm working with. Breaking down each discrete operation into its own line (and compulsively commenting everything) ensures I know what every part of the code is doing (and hopefully makes it easier for readers to understand, too).</li>
<li>Secondly, I've chosen to separate the calculation-logic from display-logic. The working-out of the length of time the content will take to read just needs to output a number. Turning that number into human-readable text is another process, and thus another function in my solution.</li>
</ul>
<p>The "Separation of Concerns" is a fundamental programming concept, and ties in nicely with the WordPress rubric: content-manipulation is for plugins, display-stuff is for themes. If I were planning to release this code, I could make a plugin to handle the calculation and let theme creators outputting the result any which way they choose.</p>
<h2 id="calculating-the-time" tabindex="-1">Calculating the time</h2>
<p>Here's my final function for calculating the read-time in seconds. This example is written in JavaScript, but only because that's what I like to write in. The general gist <em>(count the words, divide by the reading-speed)</em> can be applied to any language.</p>
<pre><code class="language-js">const readingTime = content => {
// Predefined words-per-minute rate.
const wordsPerMinute = 225;
const wordsPerSecond = wordsPerMinute / 60;
// Count the words in the content.
const wordCount = content.split(" ").length;
// How many seconds (total)?
seconds = Math.floor(wordCount / wordsPerSecond);
return seconds;
};
</code></pre>
<h2 id="displaying-the-time" tabindex="-1">Displaying the time</h2>
<p>When it comes to outputting this calculation to the screen, there are a few steps we need to take. Firstly, my guess is that very few people have an innate understanding of time when it's expressed as seconds. Thirty-seconds is a chunk of time I can comprehend, but when we go much above sixty, my instant-comprehension drops dramatically. Without thinking, do you know how long 438 seconds is? I certainly don't.<sup class="footnote-ref"><a href="#fn3" id="fnref3">[3]</a></sup></p>
<p>The simplest option would be to convert our time-in-seconds into mm:ss format. And "Minutes and seconds" is a pretty standard way to show short periods of time, so it would solve the intuitive-comprehension problem. In essence, this boils down to converting a decimal number (in <em>base ten</em>) to a sexagesimal number (<em>base sixty</em>).</p>
<p>Getting the number of minutes is easy: we just divide the number of seconds by 60 (the number of seconds in a minute), and round down to the nearest whole number:</p>
<pre><code class="language-js">const minuteCount = Math.floor(seconds / 60);
</code></pre>
<p>Getting the number of seconds is a little tricker, but here modular arithmetic is our friend. What we want is the <em>remainder</em> (a.k.a. the <em>modulus</em>) from the minute-calculation, which we get by using the modulo operator:</p>
<pre><code class="language-js">const minuteRemainder = seconds % 60;
</code></pre>
<h2 id="getting-fancy-with-our-output" tabindex="-1">Getting fancy with our output</h2>
<p>Simply showing users the minutes-and-seconds that our calculation spits out doesn't feel right to me. This is, after all, a very <em>rough</em> approximation of reading-time, as no two people read at exactly the same speed. I'd far rather show a more <em>human</em> result.</p>
<p>The simplest (but most verbose) way to get the effect I was after was to simply set a custom message if the number of seconds fell within a certain range:</p>
<pre><code class="language-js">let message;
if ( seconds < 30 ) {
message = 'hardly any time at all.';
} elseif ( seconds < 50 ) {
message = 'less than a minute.';
} elseif ( seconds < 55 ) {
message = 'nearly a minute.';
// ...
</code></pre>
<p>But this could get tiresome pretty quickly, and I much prefer a more dynamic approach. After a bit of tinkering I finally settled on a system that created a message based around how close the total-length was to a round minute. If the result is within two seconds of a minute (e.g. 122 seconds, or 239 seconds), then the result reads "<code>minuteCount</code> minutes, on the nose", and so on and so forth.</p>
<pre><code class="language-js">// ...
} else if ( minuteRemainder &lt; 2 || minuteRemainder > 58 ) {
// If we're within +/- 2 seconds of a minute:
message = `${minuteCount} minutes, on the nose.`;
} elseif ( minuteRemainder > 50 ) {
// If we're within less than 10 seconds short of any minute:
message = `just shy of ${minuteCount} minutes.`;
// ...
</code></pre>
<p>The last challenge was to convert the raw integers (<code>1</code>, <code>2</code>, <code>3</code>...) into strings (<code>"one"</code>, <code>"two"</code>, <code>"three"</code>...). To do this I've simplified <a href="http://www.karlrixon.co.uk/writing/convert-numbers-to-words-with-php">Karl Rixon's much more comprehensive solution</a>. His function handles virtually <em>every</em> number conceivable, with support for positives, negatives, decimal-points - the works! All we need to do for our current goal is convert the basic integers into strings, and we don't even need to go up very far up the number-line, either. It's unlikely I'll be writing anything that takes more than ten or fifteen minutes to read.</p>
<pre><code class="language-js">const dictionary = {}
0: 'zero',
1: 'one',
2: 'two',
3: 'three',
4: 'four',
// ... etc.
);
const string = dictionary[number];
</code></pre>
<p><em>View the full <code>convertNumberToWords()</code> function I used <a href="https://gist.github.com/tomhazledine/2964e71499c4fd28e469997933982d52">in this Gist</a>. This version uses PHP because it was written for a WordPress site.</em></p>
<h2 id="putting-it-all-together" tabindex="-1">Putting it all together</h2>
<p>With all our functions created, all that's left is to <a href="https://gist.github.com/tomhazledine/a5255b16a29ecb9f2ffb515158402f63">put it all together</a> and hook the whole thing up to the content we want to measure (in this instance the WordPress post content):</p>
<pre><code class="language-js">// Get reading time.
const readingTime = readingTime(content);
const readingTimeString = parseReadTime(readingTime);
</code></pre>
<p>Whether or not this will actually achieve anything remains to be seen. Will people click-through more when they know how much time they'll need to commit up-front? Maybe, maybe not. Either way, I've enjoyed diving into the problem (probably <em>too</em> deep).</p>
<hr class="footnotes-sep"/>
<section class="footnotes">
<ol class="footnotes-list">
<li id="fn1" class="footnote-item"><p><a href="https://medium.com/remys-blog/first-impressions-of-react-ef780fb33a60#.95xzhexqq">Medium</a> is the obvious example, but I've seen it used effectively on other sites, too. The most recent example I saw was on the <a href="https://boagworld.com/design/do-you-make-users-go-ooh">Boagworld</a> site. <a href="#fnref1" class="footnote-backref">↩︎</a></p>
</li>
<li id="fn2" class="footnote-item"><p>This is, of course, on a very trivial scale. But the idea that displaying more information (provided it's easily digestible) leads to more decisive action by users has wider implications. Most of my day-to-day work is focused around "moving the needle" on conversion rates. A percentage-point increase in click-throughs to a checkout represents a Job Well Done. <a href="#fnref2" class="footnote-backref">↩︎</a></p>
</li>
<li id="fn3" class="footnote-item"><p>A big assumption in this project is that there is a uniform reading rate, which there really isn't. Most code examples I looked at stuck to a standard words-per-minute rate of 200, but I (very unscientifically) tested my own reading-speed and found it to be closer to 300wpm. The real task at hand here is to give people a rough-estimate, so erring on the side of caution (going at the pace of the slowest) is probably the best option. I eventually settled on 225wpm as a usable middle-ground. <a href="#fnref3" class="footnote-backref">↩︎</a></p>
</li>
</ol>
</section>You are only as good as your README2016-09-28T00:00:00Zhttps://tomhazledine.com/a-good-readme/<p>Our programming languages substitute the complex logical underpinnings with easy-to-remember words. Typing <code>for</code> or <code>while</code> is an abstraction that suits our brains better than a string of ones and zeros. We program using languages <em>we</em> understand. Code is not for computers, it's for us.</p>
<p>And our code is not just written for ourselves. Other developers will need to read our code and understand it. If we are writing something useful, other people will want to incorporate it in their own work. If we are writing code for a client, somewhere down the line someone else will have to support that code. If you want your code to have a life beyond your own computer, other people will need to be able to read and understand it.</p>
<h2 id="we-write-code-for-other-developers." tabindex="-1">We write code for other developers.</h2>
<p>This is why we comment our code. This is why we use human-readable names for our variables (and let our compilation scripts reduce them to memory-efficient abstractions for us). This is why we respect coding standards and make damned-sure all our indentation is consistent and accurate.</p>
<p>This is also why the most important file in any project is not your <code>index.html</code> or your <code>package.json</code> or your <code>composer.lock</code>. The most important file in your project is your <code>README.md</code>.</p>
<p>The README file is the simplest file in your project. It doesn't control your routing or manage dependencies or run any code at all. It's nothing more than a simple text file. Your README is where you tell people what you were doing, what tools you used for what you were doing, and how you did what you were doing. And if you're feeling really generous you can tell people how they can add to what you were doing.</p>
<p>For a new set of eyes looking at your project, the README is their orientation. It's the first place they look, and it tells them where they should look next. When we write code we are writing for other developers, and if our READMEs aren't useful we may as well not be writing at all.</p>Don't turn your problem into your users' problem2016-08-23T00:00:00Zhttps://tomhazledine.com/your-problem-is-not-your-users-problem/<p>I'm noticing a tendency for organisations to pass their problems along to their customers, and I wish it didn't happen. At its simplest, this issue can be demonstrated by a basic form asking a user to input their name. It's common to see two separate fields for first-name and surname, as opposed to one box. As a developer it's easier to parse and save structured data when it's more precise, so I can see why I'd want to receive a user's name in two parts<sup class="footnote-ref"><a href="#fn1" id="fnref1">[1]</a></sup>. But all I'm doing is passing that task to the user and saving myself the tricky task of deciphering their potentially-vague input.</p>
<p>Generally speaking, anything worthwhile has complexity. The complexity has to live <em>somewhere</em>, and most of the time it's up to developers to decide where it goes. Any user-facing interface should strive to make the task as simple as possible for the user, above all other things. This is particularly relevant for e-commerce, where the simplicity of the users' experience correlates directly to money-made. Every obstacle you put in the users' path (an extra form-field to fill out, another button to press, an extra decision for them to make) can have a percentage-point impact on the conversion rate. The simpler your process, the fewer people will give up partway through.</p>
<p>Holiday letting companies are particular offenders in this area. Tied to systems that rely on fixed changeover-days but eager to maximise their availability, they resort to complex short-break rules. You can book on a Friday for three nights, provided that the preceding Monday is already booked and the booking doesn't cross a price-band boundary in the company's admin system... ouch! Making these clear to the user is a non-trivial task, and slap-dash implementations often leave the user stabbing in the dark and praying they aren't presented with yet another incomprehensible "your requested booking does not comply with short-break rules, please try again" message.</p>
<hr class="footnotes-sep"/>
<section class="footnotes">
<ol class="footnotes-list">
<li id="fn1" class="footnote-item"><p>Conversely, sometimes breaking the input into two-parts can <em>help</em>> the user. There's an argument to made for directing our users to the desired input: when presented with an ambiguous box, the user might not know whether you want their full name or just their first or last name, or their title, or maybe just their initials? Giving them a set of discrete input-fields can make their task clearer to them. This issue can also be addressed by adding sensible placeholder text to the one-box approach. This might all sound overly simplistic, but the same principles apply at all levels of complexity. <a href="#fnref1" class="footnote-backref">↩︎</a></p>
</li>
</ol>
</section>Which do you choose: native app or web app?2016-01-20T00:00:00Zhttps://tomhazledine.com/web-apps/<p>Native apps are the ones you download to a device and run as a discrete application (hence the name). So-called web-apps perform similar tasks, but run in your browser. But whether we can even consider a website to be an “app” is an altogether more contentious issue.</p>
<p>Despite what some people might say, I do think a “web-app” is a real thing. Not only that, I think web apps are one of the best things a startup (or anyone, for that matter) can invest their time in.</p>
<blockquote>
<p>There's no denying that the boundaries between web-apps and web-sites are blurry. But so are most things we try to define. Arguing about it is nothing more than a distraction.</p>
</blockquote>
<h2 id="why-is-the-existence-of-web-apps-contentious%3F" tabindex="-1">Why is the existence of web apps contentious?</h2>
<p>It's easy to understand why people see websites and native apps as separate entities. They have different underlying technologies and usage patterns, after all. And it is the <em>usage</em> that I think sways the argument in favour of web apps. They <em>are</em> websites, but we use them as if they are apps.</p>
<p>Of course, the difference only matters when you're arguing about it. Everyone else just gets on with making things. Whether you call it an ‘app’ or a ‘site’ has no impact at all on what your creation actually <em>does</em>.</p>
<p>I find I just <em>know</em> which is which. Most websites don't have that instinctive "app feel". E-commerce sites often have complex user-login areas that feel app-like. But I would class those as web-<em>sites</em>. Post some new content using WordPress and you'll encounter many full-blown app characteristics. You need to login; you're accomplishing user-centric tasks; you have your own personalised dashboard. Despite all this, I'd still call the vast majority of WordPress installs web-<em>sites</em>.</p>
<p>I pleased to report that the web-apps I concern myself with don't lie in this grey-area. I'm not saying there isn't an issue here, I'm just kicking it down the path. The fuzziness is someone-else's problem; I've got work to do.</p>
<h2 id="why-choose-one-over-the-other%3F" tabindex="-1">Why choose one over the other?</h2>
<p>One main reason native apps are so popular on iPads and iPhones is the iOS browsing experience. It sucks, big-time. Want to switch back to a tab you were on seconds ago? Sorry, you're going to have to load the page again! Looked away for a nanosecond? Load the page again! Native apps don't have this problem. Build a native app and you have full control over your users' experience.</p>
<p>Native apps can also tap into device features with more ease than something running in a browser can. Want to access files, sync with the cloud, or authenticate using an external service? All much more straightforward (for the end user) in an app.</p>
<p>It's easy to see the appeal of a native app. But there are pitfalls and gotchas for a tech startup building their first product. When using a real browser on a real computer, the web wins hands-down every time.</p>
<h2 id="why-not-have-both%3F" tabindex="-1">Why not have both?</h2>
<p>In most cases, an app will have a browser version and a native app version. This is a sign of a mature product: the users can do what they need to do wherever they feel most comfortable. Where the distinction comes into play is in the early stages of a new venture.</p>
<p>Resources are scarce early on. Budgets and deadlines will be tight. Your (small) team's technical skills will generally fall into a few key competencies. In these cases, you need to make hard decisions, if only for the sake of expediency.</p>
<p>Don't develop your minimum viable product for every platform available. That would be a staggering waste of resources. Maintaining two (or more) separate codebases will slow you down at the best of times. When you're still working on product/market fit, that slowness can be debilitating.</p>
<h2 id="this-is-where-web-apps-come-into-their-own." tabindex="-1">This is where web apps come into their own.</h2>
<p>The web just works. Sure, there may be some technical challenges. Keeping a consistent user experience across platforms is hard, especially when using cutting-edge features. But get it right and your app will run anywhere that can open a website. And that's pretty much every device out there.</p>
<p>As soon as you choose a native platform you're closing off your app to a whole swathe of the market. Develop an iOS app, and now no-one on an Android device can use your product. Or anyone on a desktop, or anyone on Windows or Linux or whatever else they might be using.</p>
<p>Make your app as a website and it will run everywhere (if you've done it right).</p>
<h2 id="how-easy-is-it-to-publish-updates%3F" tabindex="-1">How easy is it to publish updates?</h2>
<p>It's all well and good releasing Version One of your app, but you'll always need to make updates. And if you're still working towards product/market fit, regular updates are essential. The ability to stay nimble is crucial if you're working in a Lean or Agile environment.</p>
<p>If you've made a native app, pushing updates is tedious at best. Every time you want to make a change you need to go through the rigmarole of submitting your new code to the App Store. And even once you've passed the App Store code review and approval, that's not the end of the story. You still need your users to download the update (and it's far from certain they'll actually do that at all).</p>
<p>If you're publishing your own web-app, deploying a new update or bugfix is much simpler. Just push your code repository live (and maybe wait for your caches to clear). No outside approval needed. You don't even need your users' permission. With a web-app, the power lies in your own hands.</p>
<h2 id="planning-for-the-future-and-dealing-with-growth" tabindex="-1">Planning for the future and dealing with growth</h2>
<p>At some point you <em>will</em> need to choose one path over another. When you do, you should think about the time when you'll be able to branch out again. If you only have the resources to build a web-app <em>or</em> a native app, don't forget to look ahead to when you can have both.</p>
<p>If your app is a success, you'll want to spread to as many platforms as possible. The ultimate aim is, of course, to give your customers as much flexibility and convenience as you can. Alas, bouncing from one platform to another is not a straightforward process. You can't just take code written for iOS and run it on an Android device, for instance.</p>
<p>One of the steps in the creation of any decent app is the creation of a decent API. The API will power the bulk of your app's functionality, and you can plug any front-end you like into it. When you expand to other platforms, having a good API means you need not rebuild the entire app from scratch. All you need to do is build a new endpoint.</p>
<p>The API approach helps whether your going native or web-based first. But it's not a silver bullet: building the endpoint will still take time and effort. But developing an API is always easier if your endpoint prototype is web-based. So when prototyping is over, why not reuse the code you've already written for Version One of your app?</p>
<h2 id="hiring" tabindex="-1">Hiring</h2>
<p>When it comes to hiring developers to work on your project, the ubiquity of the web is your friend. Talent and experience in web apps are far more common traits than native app skills. Sticking to the fundamental technologies of the web gives you more hiring options.</p>
<h2 id="web-apps-are-the-best-option-for-getting-stuff-done%E2%84%A2." tabindex="-1">Web Apps are the best option for Getting Stuff Done™.</h2>
<p>If you have validated your great idea and are ready to build your MVP, take a serious look at building a web app. It may well save you a lot of time and effort, and will be a strong step in your journey to greatness.</p>Notes from ThingMonk: Day Two2015-12-04T00:00:00Zhttps://tomhazledine.com/thingmonk-day-two/<p><sup class="footnote-ref"><a href="#fn1" id="fnref1">[1]</a></sup>ThingMonk is a conference “for developers, designers, data wranglers and decision-makers that want to turn ideas and concepts into industrial scale systems".</p>
<p>If day one of ThingMonk was mostly about the internet of things at near-scale - IoT for our daily routine, in our houses and in our offices – day two had a more “big picture” focus to it. The headline talks were about large thorny environmental problems, and how big-industry is using IoT to make enormous efficiency savings.</p>
<hr/>
<h1 id="day-two" tabindex="-1">Day Two</h1>
<h2 id="the-ambient-kettle-project" tabindex="-1">The Ambient Kettle Project</h2>
<p>Andy Stanford-Clark, from IBM, kicked-off day two by introducing us to the <em>Hy Pi Zero</em>. The original <em>Hy Pi</em> had debuted at ThingMonk 2014 and was a Raspberry Pi powered by a hydrogen fuel cell. The <em>Hy Pi Zero</em> was the same concept, only this time using the newly-released <em>Raspberry Pi Zero</em>. It had taken the team at Arcola all the preceding day to get it up-and-running, and was now powering the slide-deck for the presentation.</p>
<p>He then handed us over to his colleague, Laura Cowen, who talked us through the "Ambient Kettle" she and Andy had been working on.</p>
<ul>
<li>It had started when Laura made her kettle post to a custom twitter account every time she made a cup of tea.</li>
<li>Her mum then started following the kettle's account, and Laura tried to think of ways to streamline the "ambient intimacy" her and her mum were experiencing.</li>
<li>By abstracting the fundamental characteristics of the experience, Laura and Andy then experimented putting those elements together independently of the act of making a cup of tea itself.</li>
<li>They identified many UX facets: the sound of the water boiling and the click of the kettle, the shape and feel of the physical object, the colour of the kettle's indicator lights as it boiled, the fact that the experience needed no configuration at all.</li>
<li>Iterating over various prototypes, they built a pseudo-kettle that severed as a remote indicator that someone (presumably someone you care about) has just made a cup of tea.</li>
<li>The end result was a tiny dolls-house scale 3D-printed kettle with an LED and speaker.</li>
</ul>
<hr/>
<h2 id="the-convergence-of-iot-and-energy" tabindex="-1">The Convergence of IoT and Energy</h2>
<p>The real show-stopper talk of the conference was given by Tom Raftery, an analyst at GreenMonk (the environment-focused arm of RedMonk; the event organisers).</p>
<ul>
<li>Energy networks are built around spikes and "peak load" (<em>Bake Off</em> finishes, and everyone up and down the country puts the kettle on and flushes the loo).</li>
<li><em>2°C</em> = cap for global warming set by 2009 Copenhagen Climate Accord.</li>
<li><em>565GtCO<sub>2</sub></em> = Gigatons of CO<sub>2</sub> needed to reach a 2°C increase (a carbon budget that leaves us with 20-30 years at current emissions rates).</li>
<li><em>2795GtCO<sub>2</sub></em> = Proven reserves.</li>
<li>We've now passed the 400 parts-per-million CO<sub>2</sub> threshold in the atmosphere, and will never cross it again. The <a href="https://scripps.ucsd.edu/programs/keelingcurve/">stats from the Scripps Institution of Oceanography</a> make for scary reading.</li>
<li>Electricity Grids are dumb. They only learn about outages when customers phone up to complain. They then need a truck-roll (apparently the biz-speak for “sending a truck”) to diagnose the problem, and then a second truck-roll with the specialist equipment to deal with the issue.</li>
<li>The grid is provisioned for 'hits' – the two weeks of the year with peak-demand dictate the infrastructure for the whole system.</li>
<li>Wind and solar are unpredictable – you can't just order-up more capacity to match demand. However, Denmark already meets 140% of its energy demand with wind-power, allowing them to sell-on excess supply.</li>
<li>Nuclear doesn't scale easily: there's a 10-20 year runway for new projects.</li>
<li>1GW is roughly the output of one nuclear power station. China are building 700GW worth of solar production.</li>
<li>Industrial IoT is often divided into two concerns: <em>Predictive Maintenance</em> and <em>Demand Management</em>. Dynamic management of energy demand could be the silver-bullet to deal with the problem of un-environmental energy production.</li>
<li>Connected devices can look at energy prices, and when the price drops suck up all the energy they can store. Increased storage capacity helps even out the miss-match between supply and demand.</li>
<li>Energy from renewables is always cheaper: fixed costs mean 'old' providers drop out of the market when the price drops below a certain point. Renewables can sell at any price.</li>
</ul>
<hr/>
<h2 id="becoming-a-digital-industrial-company" tabindex="-1">Becoming a Digital Industrial Company</h2>
<p>Jeremiah Stone from GE spoke about how his company is transitioning from a traditional heavy-industry & manufacturing company into one that fully realises the potential of 'digital'. His colleague Stephan King then gave a demo of their new IoT cloud platform (all buzz-words, little substance, very boring).</p>
<ul>
<li>GE are adding analytics infrastructure to their <em>massive</em> fleet of trains * Data fuels insight: using their new analytics, they discovered they needed to wash jet engines that operate in dusty environments, saving themselves expensive replacements (and avoiding potential fleet-grounding from the FAA) and saving $7m p.a. in fuel costs.</li>
<li>Every extra 1mph in capacity generated on the US rail network equates to $200k in savings.</li>
<li>In windfarms, turbines create a cone of turbulence behind them. Using 'smart' blades that can adjust their angle based on live data means they can reduce turbulence and increase efficiency across the whole farm.</li>
</ul>
<hr/>
<h2 id="log%2C-i-am-your-father" tabindex="-1">Log, I Am Your Father</h2>
<p>James Hodge & Matt Davies came from Splunk (one of the event sponsors). Splunk deal with data analytics.</p>
<ul>
<li>A lot of data already exists, unused, in log files: we just don't know we have it.</li>
<li>Data like this can fuel the “Build->Measure->Learn” cycle: Have an idea, build a product, measure the data that product creates, and learn from that data to generate new ideas.</li>
</ul>
<hr/>
<h2 id="we-need-to-talk-about-telco" tabindex="-1">We Need to Talk About Telco</h2>
<p>Keith O'Byrne works at Asavie, a large-scale telecommunications infrastructure company.</p>
<ul>
<li>Connectivity is a big issue for IoT projects.</li>
<li>No-one will run a copper cable into a power station. In that scenario, connectivity <em>must</em> be wireless.</li>
<li>All IoT plans start like this: “step one: assume internet”.</li>
<li>Tesla (the electric car people) addressed this problem badly - they routed the in-car WiFi hotspot through Spain. It seemed an economical solution, but had side-effects: Google was in Spanish, and location-based services (such as BBC iPlayer in the UK) would not work.</li>
<li>If you need to use a SIM in your project, use a Global Sim. They have better connectivity because they can use any network, the support and API is much better, and there's safety in numbers - most big systems use them. The only downside is they don't have a phone number, so take a bit more tech-savvy to get them working.</li>
</ul>
<hr/>
<h2 id="industry-things-at-scale" tabindex="-1">Industry Things at Scale</h2>
<p>Redmonk analyst Tomas Grassl spoke about the difference between consumer and business IoT. Consumer IoT has many areas (health, household appliances, toys, etc.) but the business space is where the revolutionary work is being done.</p>
<ul>
<li>Big ports cannot simply build new roads into big cities. They need to leverage efficiencies in order to grow. E.g. routing trucks to overflow carparks based on live data.</li>
<li>Smart bathrooms in sports-stadia allow for efficient management of supply (water, soap, loo-roll etc.) to deal with massive demand in short time-windows.</li>
<li>$447bn is spent annually on maintenance.</li>
<li>Pirelli tires for trucks now measure speed, heat, pressure, etc. They can be replaced <em>before</em> they fail, and the company has changed to a new business model: leasing the tires rather than selling them.</li>
<li><a href="https://www.youtube.com/watch?v=O1Rzyn2OIkI">James Gosling's IoT talk</a> is well worth a watch, apparently.</li>
</ul>
<hr/>
<h2 id="brought-to-you-by-the-number-ten" tabindex="-1">Brought to You by the Number Ten</h2>
<p>Recent AWS aqui-hiree Kyle Roche recreates the <a href="https://www.youtube.com/watch?v=0fKBhvDjuy0">Eames' famous Powers of Ten video</a>, but talking about IoT tech at the different scales, with various insights along the way.</p>
<ul>
<li>When automatic gunshot detectors were installed in New Jersey, authorities discovers that only 38% of gunshots were reported by humans.</li>
<li>The F-35 jet's HUD allows the pilot to see 360° 'through' the plane. It also orders its own replacement parts.</li>
<li>From source to house, the US mains loses 16% of its water.</li>
</ul>
<hr/>
<h2 id="from-killer-robot-to-killer-product" tabindex="-1">From Killer Robot to Killer Product</h2>
<p>Pat Patterson is a “Developer Evangelist Architect” for event-sponsor SalesForce. His job sounds amazing: traveling from event to event showcasing interesting techy things.</p>
<ul>
<li>A snazzy demo can be very useful and powerful as a tool to drive engagement (his main example was an industrial robotic arm, normally used for manufacturing, that he'd taught to do a dance).</li>
<li>We're living in an age of cheap components - creating a working prototype for an IoT project can be very quick and very affordable.</li>
</ul>
<hr/>
<p><sup class="footnote-ref"><a href="#fn2" id="fnref2">[2]</a></sup>On the whole, ThingMonk 2015 was amazing. I met loads of people working on all sorts of interesting projects, and it hammered home to me that the IoT revolution has already started. I've come away inspired to get even more stuck-in to my IoT side-projects, and with a feeling that it is now essential to start bringing IoT concepts into my day-to-day work too.</p>
<hr class="footnotes-sep"/>
<section class="footnotes">
<ol class="footnotes-list">
<li id="fn1" class="footnote-item"><p>These are my notes from day two of the conference. If you want to catch-up, read <a href="https://tomhazledine.com/thingmonk-day-one">my notes from the first day of ThingMonk</a>. <a href="#fnref1" class="footnote-backref">↩︎</a></p>
</li>
<li id="fn2" class="footnote-item"><p>If you missed them, don't forget to check out <a href="https://tomhazledine.com/thingmonk-day-one">my notes from day one of ThingMonk</a>. <a href="#fnref2" class="footnote-backref">↩︎</a></p>
</li>
</ol>
</section>Notes from ThingMonk: Day One2015-12-03T00:00:00Zhttps://tomhazledine.com/thingmonk-day-one/<p><sup class="footnote-ref"><a href="#fn1" id="fnref1">[1]</a></sup>ThingMonk was wall-to-wall interesting and inspiring. I took lots of notes, but naturally focused on the areas that interested me most, and was often too busy trying to take it all in to write anything down...</p>
<p>ThingMonk styles itself as a conference “for developers, designers, data wranglers and decision-makers that want to turn ideas and concepts into industrial scale systems”.</p>
<p>I was there with my web-developer hat on, but was keeping my eyes peeled for hints at the future of business-tech in general. I'm already deeply embedded in data-visualisation and dynamic analytics, but eager to connect my digital skills to a hardware framework.</p>
<h1 id="day-one" tabindex="-1">Day One</h1>
<h2 id="bullet-proof-%26-profitable-things" tabindex="-1">Bullet-Proof & Profitable Things</h2>
<p>Ubuntu guru and part-time space tourist <a href="http://canonical.com/">Mark Shuttleworth</a> made for a high-profile, big-picture opening speaker. It was interesting to hear that his idea of "scale" was not massive super-computers costing exorbitant amounts of money, but rather lots and lots of cheap, tiny micro-computers all working in unison over a network.</p>
<ul>
<li>It takes five to ten years to make an overnight success, and <em>now</em> is the time to be getting deep into IoT. Wait 12 months and the train will have left the station.</li>
<li>Real disruption comes from riding a wave of change, and the disaggregation and componentisation we're seeing in the IoT sphere at the moment will inevitably lead to the "hockey stick" graph of innovation.</li>
<li>Data can be a liability - you must have a responsible security strategy when handling lots of data. One mistake or hack can cripple a big-data company.</li>
<li>The next version of Ubuntu will be highly modular. Apps will be isolated and contained, and will each have a predefined area they can write to. This allows for robust version control across the system, and no more fear when updating the OS.</li>
<li>The “Internet of Toys” is going to be more influential than people realise. It's easier to innovate with things that won't kill people. Long-term projects will have to use standards already set by short-term projects.</li>
</ul>
<hr/>
<h2 id="coherent-ux-for-distributed-systems" tabindex="-1">Coherent UX for Distributed Systems</h2>
<p>User-experience expert <a href="http://www.designingconnectedproducts.com/">Claire Rowland</a> had some fascinating insights into the challenges of managing UX in a world of connected "things".</p>
<ul>
<li>We don't expect "things" to behave like the internet. Light bulbs shouldn't "buffer".</li>
<li>Interfaces (if they are honest) should introduce the concept of failure to users. Don't fake success when a user clicks a button in a low-connectivity area: show in interstitial "loading" state wherever possible.</li>
<li>You often have to choose between fuzzy-and-current or accurate-but-old. A cat-tracker can either say "Mrs. Tibbles was at this exact location 3 hours ago" or "Mrs. Tibbles is somewhere within this range right now".</li>
<li>Linking physical and digital interfaces takes special thought. A traditional dimmer-switch always knows it's ‘state’, where as a remotely-controlled version has to account for the fact that the ‘state’ of the bulb might have changed since it last checked.</li>
</ul>
<hr/>
<h2 id="from-2-to-2-billion%3A-how-to-design-for-scale." tabindex="-1">From 2 to 2 billion: How to Design for Scale.</h2>
<p>Sam Winslet & Sophie Riches from IBM presented us with the "Seven Deadly Sins for Design of IoT":</p>
<ul>
<li>Greed: designing for yourself.</li>
<li>Envy: being a copycat.</li>
<li>Gluttony: focusing on features & functions.</li>
<li>Wrath: assuming your users know what IoT is.</li>
<li>Lust: IoT for the sake of it, without any real ideas.</li>
<li>Pride: don't over-complicate and don't over-simplify. IBM use a strategy they call “Progressive Disclosure”: where an interface reveals deeper levels of detail as a user interacts with it.</li>
<li>Sloth: assuming users have ∞ patience or care about you/your product.</li>
</ul>
<hr/>
<h2 id="hacking-nfc" tabindex="-1">Hacking NFC</h2>
<p>Nick Ludlum from <a href="%5BMoo%5D(http://moo.com/)">Moo</a> gave (with a little help from colleague Kai Turner) one of the more engaging and tech-focused presentations of the event. The team at Moo have (after considerable time spent in R&D) figured out how to print NFC chips into business cards.</p>
<p>Every attendee at the conference was given a Moo business card on arrival, which was embedded with an NFC chip with tokens for free coffee and beer.</p>
<ul>
<li>For NFC to work, you need a chip and an antenna. The chip is tiny – literally mm across – and the antenna gets printed between to laminated sheets of paper using conductive silver ink. This means the finished business card looks and feels just like a regular card - after all, it's still mostly just card and ink.</li>
<li>To build a reader, all you need is a "smarter" chip, an antenna, and a power source. Android phones have the ability to be NFC readers, but IOS ones do not.</li>
<li><code>nfcpy</code> is the Python library for NFC.</li>
<li>The cards can store a variety of MIME-type information (webpages, lat. & lng. data, etc) but URLs are the most versatile (and can be re-purposed without reprogramming the card itself).</li>
<li>Security is still ongoing project, and the Moo team would welcome any insights or help in that area that the developer community can provide.</li>
</ul>
<hr/>
<h2 id="the-things-network%3A-london" tabindex="-1">The Things Network: London</h2>
<p>Mark Hill, from <a href="http://opentrv.org.uk/">Open TRV</a> (a company that makes smart Thermostatic Radiator Valves) got the short-straw: his talk was right before lunch, and therefore had to be shortened because the conf. was running over time.</p>
<ul>
<li><a href="http://thethingsnetwork.org">The Things Network</a> is a low-power, wide-area network.</li>
<li>Crowdsourced a complete city-wide IoT data network in Amsterdam.</li>
<li>Launching the network in cities across the world. London had its network launched this month.</li>
<li>Powered by gateways installed on roofs of businesses, accessible to all. ($1000 - $1500 per gateway).</li>
</ul>
<hr/>
<h2 id="data-gravity-%26-time-series" tabindex="-1">Data Gravity & Time-series</h2>
<p>Dave McCrory, allegedly famous for for coining the term “data gravity”, delivered an engaging talk with a charming hand-drawn slide-deck. He touched briefly on how data != information, but his main point was that dealing with time (in an IoT setting) is important but very, very hard.</p>
<ul>
<li>Data Gravity: data has a 'weight' and can be hard to deal with. Some datasets might take 40 years to even upload, let alone process.</li>
<li><code>0</code> is just data. <code>0°C</code> is data with context. <code>0°C in London today</code> is information.</li>
<li>When you are working with real-time analytics, how do you deal with that data? System clocks become important - do you trust your machine, your cloud server, or the machine in the field?</li>
<li>Do you deliver a constant stream of data? Sometimes it might be better to time-stamp it and package it up, then send packets of data at regular intervals. That leads to less chance of drops in connection causing data loss.</li>
</ul>
<hr/>
<h2 id="the-secret-life-of-buildings" tabindex="-1">The Secret Life of Buildings</h2>
<p>It was interesting to head Yodit Stanton's insights into working with “connected“ buildings, particularly some of the pitfalls and unexpected issues.</p>
<ul>
<li>Her company <a href="https://www.opensensors.io/">opensensors.io</a> deals with commercial spaces with pre-existing Building Management Systems (BMSs).</li>
<li>Automation in a machine-to-machine sense (M2M) has been around for years: office lights that turn on when you enter the room, automatic doors, etc.</li>
<li>Learning about a building's 'health' in the form of lighting-levels, environmental factors, footfall, etc. can give you insight into employee absenteeism, efficiency of your workforce, and on and on...</li>
<li>Average cost of one desk in London is £12k p.a. Small percentage increases in efficiency can reap big rewards.</li>
<li>Legacy protocols (<code>bacnet</code>, <code>dali</code>, <code>modbus</code>, <code>knx</code>) are building-data standards and will be around for a while. Contracts are typically 15-20 years long, and cost between £50k and £200k. They won't be thrown out anytime soon.</li>
<li>They lock down their data too much. None of those services have an API.</li>
<li>Open Source integrations are more sustainable and easier to manage.</li>
<li>Don't use WiFi - there are “edge dependencies” on people (in one example, an office manager unplugged the wi-fi router every night!).</li>
</ul>
<hr/>
<h2 id="it's-none-of-your-effing-business" tabindex="-1">It's none of your effing business</h2>
<p>The award for most curmudgeonly speaker at the event goes to Boris Adryan (according to ThingMonk host James Governor). Boris devoted a large portion of his talk to dissecting an article depicting a <a href="http://knowledge.openboxsoftware.com/blog/the-evolution-of-business-intelligence">near-future IoT-fueled wakeup routine</a>. The post is well-worth a read.</p>
<p>He also touched on machine learning, and I started craving a cup of tea and took fewer notes... In short, we need our systems to be smart because people instinctively lack an understanding of statistical p-values.</p>
<hr/>
<h2 id="things%2C-people%2C-and-beer" tabindex="-1">Things, People, and Beer</h2>
<p>Craig Cmehil, from <a href="http://go.sap.com/uk/index.html">SAP</a>, subtitled his talk “voice as an alternative user interface for analytics”. He gave a demonstration of the voice-activated Amazon Echo, essentially a programmable Siri in a large box.</p>
<p>He ran a mini competition, too, where he gave away an Echo (not yet available in Europe) which involved some live-coding: always a brave mood in a live presentation...</p>
<hr/>
<h2 id="iot-device-management" tabindex="-1">IoT Device Management</h2>
<p>Juan Perez (from Microsoft) said some stuff about Azure (a new MS Cloud platform or something-or-other). I may have slept through this one...</p>
<hr/>
<h2 id="welcome-to-the-conversation" tabindex="-1">Welcome to the Conversation</h2>
<p>The official father of the term “Silicon Roundabout”, Matt Biddulph, closed day one with a talk about how 'conversation' can help solve some of the main issues with many IoT devices. He was very coy about his new venture, <a href="http://thington.com/">Thington</a>, but hinted that it will be using this concept of conversation in conjunction with the “social graph” when it launches in early 2016.</p>
<p>Traditional light-bulbs are an interface an messaging bus. A light-switch is a state messaging transport. Adding connectivity to a bulb (<em>a la</em> Philips Hue) adds issues:</p>
<ul>
<li>latency</li>
<li>state</li>
<li>consensus</li>
<li>trust</li>
<li>location</li>
<li>power</li>
</ul>
<p>There's an 'old' scholarly article that outlines the 8 fallacies of distributed computing:</p>
<ol>
<li>The network is reliable.</li>
<li>Latency is zero.</li>
<li>Bandwidth is infinite.</li>
<li>The network is secure.</li>
<li>Topology doesn't change.</li>
<li>There is one administrator.</li>
<li>Transport cost is zero.</li>
<li>The network is homogeneous.</li>
</ol>
<p>Matt's “New fallacies of Distributes <em>Services</em>”:</p>
<ol>
<li>Devices are powered.</li>
<li>Devices are reachable.</li>
<li>Devices stay in one place.</li>
<li>Messages can be trusted.</li>
<li>Causality is unambiguous.</li>
<li>All device state is known.</li>
<li>There is one clock.</li>
</ol>
<p>How do we solve these issues? Simple: a conversation. If the devices can 'talk' to one-another, they can iron out inconsistencies and neutralize many of the problems.</p>
<hr/>
<p><s>At some point in the next week or so (mid-late Dec 2015) I'll post my notes from day 2 of ThingMonk. I'll add the link here, but if you're really desperate not to miss out, drop me a line (tom@tomhazledine.com) and I'll add you to the slowly-growing list of people who I'll email when I've published the notes.</s> <sup class="footnote-ref"><a href="#fn2" id="fnref2">[2]</a></sup></p>
<hr class="footnotes-sep"/>
<section class="footnotes">
<ol class="footnotes-list">
<li id="fn1" class="footnote-item"><p>These are my notes from day one of the conference. You can read my <a href="https://tomhazledine.com/thingmonk-day-two">notes from the second day here</a>. <a href="#fnref1" class="footnote-backref">↩︎</a></p>
</li>
<li id="fn2" class="footnote-item"><p>You can read my <a href="https://tomhazledine.com/thingmonk-day-two">notes from the second day here</a>. <a href="#fnref2" class="footnote-backref">↩︎</a></p>
</li>
</ol>
</section>Why doesn’t everyone have an Internet Fridge?2015-08-05T00:00:00Zhttps://tomhazledine.com/internet-fridge/<img class="post-content__image" src="/images/articles/fridge.png" alt="Heath Robinson internet fridge"/>
<p>The great white hope of the Internet Of Things is the internet-connected fridge. It's the go-to example when trying to explain IOT to lay people. "Imagine what you could do if your fridge was connected to the web!" So why don't we have them yet? And do we really want them? Or even need them?</p>
<p>The "Internet of Things" is not about adding the internet to devices. Being able to access the internet through a fridge gives us nothing. There's no earthly benefit to having your social-network connections "like" your fridge, and there are few scenarios where you'd want to "share" your #fridge.</p>
<p>More accurately, there are few occasions when I'd want you to share your fridge with me. In person, it's different. I definitely want you to share: I'll have a cold beer, please, and maybe a sandwich? Digitally, however? No way.</p>
<p>The noughties idea of an internet-connected device was simple. We can access the internet wherever we want: access email through the TV, get the news from your microwave, do your shopping through the fridge! Simple, but pointless once smartphones achieved ubiquity. Why wait for the fridge to boot-up when my phone's already in my pocket?</p>
<p>There's no aspect of the old, clichéd example of an "internet-fridge" that you couldn't better achieve by glue-gunning an iPad onto the front of a regular fridge. And that would be more effective and cheaper than a monstrous over-priced, internet-enabled fridge.</p>
<h2 id="where's-the-win%3F" tabindex="-1">Where's the win?</h2>
<blockquote>
<p>Where things get really interesting, and where IOT really comes into its own, is when we switch from accessing the internet through our devices to accessing our devices through the internet.</p>
</blockquote>
<p>If a component moves, it will heat up. If it heats up, it will distort and eventually wear-out. Tiny sensors are now cheap and easily available, and connecting them to the internet is also cheap and easy. It's the perfect-storm to herald the age of IOT. Put a sensor on something that moves and connect it to the internet, and suddenly you can make efficiency savings.</p>
<p>At scale, this is for wheels and gears on industrial machinery. If you know when a component will break, you can replace or fix it before it breaks. This knowledge will save you time and money. In the consumer marketplace, the wins are less tangible, but more relateable.</p>
<h2 id="how-would-this-manifest-itself-in-a-fridge%3F" tabindex="-1">How would this manifest itself in a fridge?</h2>
<ol>
<li>The cooling mechanism inside the fridge has broken: sound the alarm!</li>
<li>The freezer door has been left open for more than 10 minutes: text alert!</li>
<li>You're shopping, but can't remember what you have already: a webcam snapshot of the inside of your fridge gets sent straight to your phone.</li>
<li>You live in the future, where every consumer good has a sensor inside the packaging. Your fridge acts as a hub that sends useful data out to you. The milk carton is 32% full. The quiche is 2-days 7-hours away from its Best Before date. The veg drawer is giving off a high proportion of noxious gases: I guess the peppers have gone off; here's a photo so you can decide for yourself.</li>
</ol>
<h2 id="why-hasn't-it-happened-yet%3F" tabindex="-1">Why hasn't it happened yet?</h2>
<dl>
<dt>1: The runway for white-goods is too long.</dt>
<dd>Iteration takes decades. Traditional IOT needs a much much shorter iteration cycle. By the time a traditional incumbent gets a product to market it has become so overwhelmed by extra features and catches for obscure edge-cases that the primary objectives have been lost. Simplicity is just not an option for these companies, and that it why they will always fail.</dd>
<dt>2: People buy fridges very infrequently.</dt>
<dd>If an incumbent manufacturer get their product in your home, it’s going to be there for ten years or more. Give that product the ability to cross-sell other products, and that incumbent has a marketing beachhead in your kitchen. That’s not something they’ll be able to resist. If you get an Internet Fridge from an established white-goods manufacturer, your fridge will spam you.</dd>
<dt>3: White-goods manufacturers are not software companies.</dt>
<dd>“Smart” thermostats, for example, have been around for years but have always been ugly and nigh-on unusable. Nest are not doing anything particularly new, but they have an eye for details and user experience, and are confident in their own value. It’s the Apple effect all over again. Only one company in every thousand is actually any good. Getting “normal” people to interact seamlessly with technology is a tough sell — can your mum use the video recorder yet?</dd>
</dl>
<blockquote>
<p>Making user-friendly software is hard. Making user-friendly hardware is hard. Doing both together is even harder, by many orders of magnitude.</p>
</blockquote>
<figure class="post-content__image-wrapper">
<img class="post-content__image" src="/images/articles/fridge2.png" alt=""/>
<figcaption class="post-content__caption">The unintuitive interface from the depths of hell that I have to decipher every time I want to bump up the heat, and a Nest thermostat that makes me wish my life was more like the fancy people I see in magazines.</figcaption>
</figure>
<dl>
<dt>4: White-goods manufacturers do not sell to consumers: they sell to retailers.</dt>
<dd>They don’t understand their end-users in the way that a regular customer-facing company does. Without that understanding, they’ll never make an innovative product that can truly excite us.</dd>
</dl>
<img class="post-content__image" src="/images/articles/fridge3.png" alt=""/>
<dl>
<dt>5: It increases the cost across the board.</dt>
<dd>Adding IOT capability makes the products more expensive, and only a fraction of the current market would see any real value from them. I know from personal experience that only a small percentage of people can see the potential of the internet , let alone the Internet of Things. Commercial success in this field is going to take a revolution in consumer-mindset.</dd>
<dt>6: The business model doesn’t suit the incumbents.</dt>
<dd>Another often-overlooked aspect of “The Internet” is that it’s not free and it does exist in a physical world. It needs infrastructure and servers, and what that means in business terms is recurring costs.
Suddenly your white-goods manufacturer has to deal with support and maintenance. That’s not what they signed up for, that’s not where their expertise lies. They’ve spent decades implementing “planned obsolescence”.
To justify implementing IOT into their products, these companies will have to be sure of generating the recurring revenue to match the recurring costs. Which brings us back to the fridge that sends you spam…</dd>
</dl>
<h2 id="so-we-need-a-start-up-to-make-the-internet-fridge%3F" tabindex="-1">So we need a start-up to make the Internet Fridge?</h2>
<p>To create the Internet Fridge we all want needs serious innovation, and all the evidence suggests the big incumbent white-goods manufacturers just aren’t up to the task. This is where the mythical Start Up™ can help. Those guys live for innovation and disruption, right? Well, maybe…</p>
<p>To bootstrap an idea into existence you need a short iteration cycle. Try something, watch it fail, learn from the failure, improve the next version. Try something, fail, learn, improve. Try, fail, learn, improve. And so on. It’s a pattern that has worked for almost every successful start-up out there.</p>
<p>This approach works really well in the software and internet spheres where you can ship updates and bug-fixes as often as you like. With hardware, things get much messier. There’s no continuous deployment or integration for fridges. If your first version doesn’t work, you need a product recall.</p>
<p>And just getting to that first version in the first place is hard, for all the reasons we outlined above. The lead-times are long and manufacturing is only cost-effective at scale, so rapid-prototyping is both hard and expensive.</p>
<h2 id="what-we-need-is-an-apple-for-white-goods." tabindex="-1">What we need is an Apple for white-goods.</h2>
<p>We need a large established company that values good design, leads the field in innovation, and isn’t afraid of changing a premium for quality. They need to have experience in manufacturing, and also be at the top of their game with software too. They need deep pockets and a long-term vision.</p>
<p>There are companies out there that could do it. Occulus are working at the intersection of hardware and software: investing in their headsets and the digital systems that power them. Nest are pushing the boundaries of what we expect from a home appliance: beautiful and functional.</p>
<p>Traditional white-goods — fridges, freezers, dishwashers, washing-machines — they all suffer from being both hard to build and not very sexy. Whoever creates the perfect Internet Fridge will no doubt make a pretty penny, but with such imposing obstacles in the way, I can’t see anyone getting there any time soon.</p>Getting started with inline SVG icons2015-05-31T00:00:00Zhttps://tomhazledine.com/inline-svg-icons/<p>I've long been an advocate of using icon fonts. They're resolution-independent, light-weight, and stylable with CSS. It turns out they're not the best option: inline SVG icons are better in almost every way.</p>
<p>We should all know by now that using raster images for icons is a bad idea. Limiting ourselves to one set resolution is not how we work in a multi-device ecosystem. It's not responsive, it's not performant, it's not nice.</p>
<p>For a while it seemed that Icon Fonts were the answer. They're vector based and therefore resolution-independent. You can style them with CSS. They have smaller file sizes than .JPGs or .PNGs, so they improve a site's performance. A font file is one asset to download (more http requests = poorer site performance), so we don't need to mess around with image sprites any more. And on top of all that, icon font creation can be easily automated with your task-runner of choice: simply drop your icon .SVGs into a folder and you're good to go.</p>
<pre><code class="language-css">/* Calling an icon font icon as a pseudo element within CSS. */
.elementName:before {
font-family: "iconfont";
content: "\e001"; // The unicode value of the font character you want.
color: #000; // The icon is text, so you can style it using regular CSS
}
</code></pre>
<h2 id="if-it-ain't-broke%2C-why-fix-it%3F" tabindex="-1">If it ain't broke, why fix it?</h2>
<p>Lately the winds of change have been blowing. Fewer and fewer big-name sites are using icon fonts, and more and more are publicly coming out in favour of inline SVGs. At first I was sceptical: "that problem's already been solved", I thought. Actually inlining raw SVG code into a site seemed overly complicated, and harder to maintain and automate than an icon font setup. But was it? And were icon fonts as great as they first appeared?</p>
<dl>
<dt>When icon fonts fail, things get weird.</dt>
<dd>Sometimes there's no telling what crazy character a browser will display in place of an icon that hasn't loaded.</dd>
<dt>There are inexplicable rendering inconsistencies.</dt>
<dd>Firefox in particular seemed, like an overly generous grandparent, determined to fatten my icons up. Faux-bold on regular fonts is ugly and annoying, but when it happens to brand assets like icons it's excruciating (and something clients <em>always</em> pick up on).</dd>
<dt>Automation isn't so seamless after all.</dt>
<dd>Oh hey, my <code>npm install</code> keeps failing. I wonder what the problem is: oh yeah, my <code>gulp-icon font</code> is throwing errors all up in my grill. Again. #facepalm</dd>
</dl>
<p>By contrast, pure SVG is a much more consistent format. I've been using SVGs within <code><img /></code> tags since 2006, and I've never had any trouble with setting SVGs as CSS background images.</p>
<pre><code class="language-css">/* SVG CSS background-image. */
.elementName {
height: 1em;
width: 1em;
background-image: url(path/to/icon.png); // Fallback for browsers that don't like SVG.
background-image: url(path/to/icon.svg);
}
</code></pre>
<p>The obvious <em>simple</em> solution is to just use SVGs like that: set them with CSS or use <code><img></code> elements. That gives you scaleable vector graphics that work well in most environments. The trouble is, you're loading in a new file for every icon. Every icon would represent another HTTP request to the server, and would be a performance nightmare. No matter how "squishy" your design, if your site is slow to load it is <em>not</em> "Responsive".</p>
<p>So we're back to the same multiple-requests problem we had in the bad old days of raster iconography. The answer back then was to use image sprites. Concatenate all the icons into one file, load them all with just one HTTP request, and use CSS to crop the image when you want to display a particular icon. It turns out we can do that with SVG too, except now we can do it better!</p>
<h2 id="inline-svg-sprites" tabindex="-1">Inline SVG Sprites</h2>
<p>SVGs are made of paths – collections of co-ordinates that describe the individual vectors of the image. Open an SVG in your favourite text editor and you can see those paths. What's more, you can add classes and IDs to them. Even more mindblowing: if you wrap the individual paths in a <code>symbol</code> tag you can combine multiple images into a single SVG file, and selectively pull them out using their IDs.</p>
<pre><code class="language-html"><!-- Load the SVG file into your document... -->
<svg
xmlns="http://www.w3.org/2000/svg"
xmlns:xlink="http://www.w3.org/1999/xlink"
>
<symbol viewBox="0 0 10 10" id="iconID">
<path d="{ vector data }" />
</symbol>
</svg>
<!-- ...and then 'use' a specific icon -->
<svg class="icon">
<use xlink:href="#iconID" />
</svg>
</code></pre>
<p>Now you might be thinking that this all looks like a whole lot of work, and you'd be right. Making sprites by hand sucks. Thankfully the arrow of progress is on our side. We may not have jetpacks and flying cars yet, but we <em>do</em> have task runners. I used to use Grunt, but now I use Gulp (being a JS nerd, I find the syntax easier to follow) and there are SVG spriting plugins for both of them. I use <code>gulp-svg-sprite</code>, and so far it's worked like a charm (so far I've put it to work on a half-dozen private projects and a couple of client sites). In fact, it's proving to be much more reliable than any of the Gulp plugins for icon fonts ever were.</p>
<p>It takes a bit of configuring to get the drop-an-icon-into-a-folder-and-let-Gulp-do-the-rest workflow I'd maintained with icon fonts, but as long as you remember to specify that you want a <code>symbol</code> based sprite, you can't go too wrong.</p>
<pre><code class="language-js">// My Gulp config
var svgConfig = {
shape: {
dimension: {
// Set maximum dimensions
maxWidth: 32,
maxHeight: 32
}
},
mode: {
symbol: true // Activate the «symbol» mode
}
};
// The task itself
gulp.task("svg", function () {
return gulp
.src("_uncompressed/icons/**/*.svg")
.pipe(svgSprite(svgConfig))
.pipe(gulp.dest("_includes"));
});
</code></pre>
<hr/>
<h2 id="inline-svg-in-jekyll" tabindex="-1">Inline SVG in Jekyll</h2>
<p>This site is built using the static-site generator Jekyll. Transferring this site's (admittedly meagre) icon set from an icon font to an inline SVG sprite couldn't have been easier. Make sure your task-runner outputs your sprite into Jekyll's <code>_includes</code> directory and you can pull in your sprite with a <code>{% include /symbol/svg/sprite.symbol.svg %}</code>. And because Jekyll compiles <em>before</em> deployment, you don't even have to make a single HTTP request to get your icons: it's all truly inline.</p>
<h2 id="inline-svg-in-wordpress" tabindex="-1">Inline SVG in Wordpress</h2>
<p>Jekyll's all well and good if your clients are happy with working in code and using the command line to update their site. Sadly the world hardly ever makes things <em>that</em> easy for us. Wordpress sites don't have the advantage of being compiled before deployment, which means we need to use PHP to include the sprite.</p>
<p>Including an automatically generated SVG sprite using PHP is a little trickier than it first appears. <code><?php include_once("path/to/svg/sprite.svg"); ?></code> would seem like the obvious solution, but all that will give you is a face full of errors (if you've got <code>WP_DEBUG</code> set to <code>true</code>). It turns out the issue is with the XML and <code>DOCTYPE</code> added to the sprite by the gulp task, and the trick is to use <code>file_get_contents()</code> instead of <code>include_once()</code>.</p>
<h2 id="svg-%26-css" tabindex="-1">SVG & CSS</h2>
<p>Styling the icons had a couple of little hurdles that I wasn't expecting. One of the joys of icon fonts was how easy they were to manipulate with CSS, and that's a feature that SVG sprites were promising too. Instead of <code>color: #000;</code> we use <code>fill: #000;</code> but, as with all CSS, watch out for specificity! If the paths themselves have explicit fills and strokes defined, then you won't be able to override them with external CSS.</p>
<p>Some of my legacy icons have in-built colours, and going through and stripping them out would be a real pain. Thankfully my buddy <a href="http://twitter.com/enshrined">Daryll</a> was on hand to build me a tidy RegEx to programatically strip out any rogue fills:</p>
<pre><code class="language-php"><?php
// Save the SVG file in a variable...
$rawSVG = file_get_contents(get_template_directory_uri() . "/path/to/svg/sprite.svg");
// ...then filter out any fills.
echo preg_replace( '/fill=("|\')(#)?([a-fA-F0-9]*)("|\')/i', '', $rawSVG );
?>
</code></pre>
<p>When I'm using Jekyll I still have to manually manage explicit in-file styling, but Jekyll projects tend to require a more fine-grained approach anyway. Another Big Win for CSS styling and SVGs comes from being able to add classes and IDs to specific paths <em>within</em> individual icons. This provides us with the hooks we need to style (and animate and manipulate) almost every aspect of an icon discretely. Icon fonts were limited to one flat colour, but with SVG sprites we can colour every shape in an icon differently (should we need to).</p>
<p>For a long time I was very skeptical of using inline SVG for icon sets, primarily because of the perceived effort it would take to set up such a system. I'm happy to report that SVG sprites are considerably easier to get started with than I'd ever thought. I can't imagine ever using an icon font again.</p>Inspirational Web People2014-09-09T00:00:00Zhttps://tomhazledine.com/inspirational-web-people/<p>There's an <a href="http://boagworld.com/season/10/episode/1007/">episode of the Boagworld podcast</a> where Marcus and Paul list the ten ‘web people’ they've found most <em>inspiring</em>. This has in turn inspired <em>me</em> to publish my own list.</p>
<p>The web-creators community is packed with generous people willing to share their insights and experiences. Through blog posts and podcasts, conference talks and tutorials, these people have had a direct impact on my outlook and my career choices.</p>
<h2 id="01%3A-steve-lovegrove" tabindex="-1">01: <a href="http://stevenlovegrove.com/">Steve Lovegrove</a></h2>
<p>In my experience everyone has that one person that inspired them when they were just starting out. For some it's a teacher, or someone who gave them their first break, or a professional who mentored them. For me, it was my friend Steve.</p>
<p>Steve's always been miles ahead of me in intellect and ability, and most of what he tried to teach me went straight over my head. But even so, he was willing to let me spend hours peering over his shoulder while he coded. And because we would also talk through what he was working on, some of it rubbed off.</p>
<p>Steve taught me just enough to be dangerous; just enough to get something on the web and see how good it felt. He's long since left the web behind to do ‘real’ science, but having a patient and generous friend like Steve was probably the single biggest factor in shaping the direction my career has taken.</p>
<h2 id="02%3A-chris-coyier-and-dave-rupert" tabindex="-1">02: <a href="http://twitter.com/chriscoyier">Chris Coyier</a> and <a href="http://twitter.com/davatron5000">Dave Rupert</a></h2>
<p>Anyone who's ever Googled a CCS question has probably benefited from Chris' sterling work on his website <em><a href="http://css-tricks.com/">CSS Tricks</a></em>, but it's his video tutorials (on Lynda.com and the “Lodge” section of his own website) that have impacted my world the most.</p>
<p>His approach of <em>getting-it-wrong-on-camera-and-then-fixing-it-(probably)</em> was a welcome break from other more sanitised tutorials. Not everything installs perfectly the first time, and <em>no one</em> is immune from typos. Seeing how professionals deal with these problems is almost as useful as the main focus of the lessons.</p>
<p>Chris also does sterling work in the podcast field with his partner in crime Dave Rupert. If you're not entertained by the combination of in-depth Q-and-A, stupid sound effects, and friendly banter that they put out in the <em><a href="http://shoptalkshow.com/">Shop Talk Show</a></em>, then you're probably not interested in front-end development.</p>
<p>As an added bonus, their mantra is probably the single most useful bit of advice any web professional can give:</p>
<blockquote>
<p>Just. Build. Websites.</p>
</blockquote>
<h2 id="03%3A-brad-frost" tabindex="-1">03: <a href="http://twitter.com/brad_frost">Brad Frost</a></h2>
<p>I'm never more aware of (or excited by) the unique nature of designing for the web than when I listen to Brad Frost. If every colleague and every client could take the time to <a href="http://bradfrostweb.com/blog/">read his blog</a> and <a href="https://www.youtube.com/watch?v=nE0CRMm59BY">watch his talks</a>, the standard of our work and the quality of our work-lives would raise ten-fold across the board.</p>
<p>The idea of designing ‘paintings of websites’ (a.k.a. PSD mockups) is completely outdated; we can't just design for one ideal viewport size, or two, or ten, or a hundred. “Atomic Design” might sound like a buzz word, but the principles are applicable to all web work. The basic concept of designing components first and pages last is quite simply How It Should Be Done Now.</p>
<h2 id="04%3A-mark-brickey%2C-james-flames%2C-billy-bauman" tabindex="-1">04: <a href="http://twitter.com/markbrickey">Mark Brickey</a>, <a href="http://twitter.com/thejamesflames">James Flames</a>, <a href="http://twitter.com/DeliciousDL">Billy Bauman</a></h2>
<p>I love to hear people talking candidly and in-depth about fields they are experts in. It almost doesn't matter what the topic is (which is one of the reasons I love Radio 4's <em>In Our Time</em> so much), but when the topic is illustration I love it even more. The <em><a href="http://www.aidpodcast.com/">Adventures In Design</a></em> podcast is irreverent and unhinged, but always honest and informative.</p>
<p>They always have good guests, but more importantly Mark, Billy and James know what to do with them. They trigger interesting and inspirational discussion, no matter who the guest is. The resulting interviews are often challenging, often controversial, and always riveting listening.</p>
<p>Another reason I like AID so much is that team aren't ashamed of making a living: this is a podcast about the <em>business</em> of design. It just so happens that the design field they work in is the super-cool one of gig posters and illustration.</p>
<h2 id="05%3A-mike-monteiro" tabindex="-1">05: <a href="http://twitter.com/monteiro">Mike Monteiro</a></h2>
<p>Anyone working in the world of design who deals with clients should watch Mike's *<a href="http://vimeo.com/22053820">F*ck You. Pay Me.</a>* talk. He explains why it's important to take yourself and your business seriously, and even brings his lawyer on stage.</p>
<p>His book, <em><a href="http://www.abookapart.com/products/design-is-a-job">Design Is a Job</a></em>, is less caustic, but just as inspiring. His book, Design Is a Job, is less caustic, but just as inspiring. Neither the book nor the talk are rants against stupid clients (well, maybe a little...). They focus instead on building <em>productive</em> client relationships where no-one is taken advantage of.</p>
<p>It's hard to admit that sometimes a problem doesn't stem from the client but from yourself. Even so, having that in the back of my mind has often saved my freelancing-bacon. Following Mike's advice has often turned tricky clients into excellent ones. For all his acetic bluster, Mike's core message is simple; be friendly and use common sense.</p>
<h2 id="06%3A-jessica-hische" tabindex="-1">06: <a href="http://twitter.com/jessicahische">Jessica Hische</a></h2>
<p>Jessica is a letterer, and all her work is stunning. Before I was aware of her, I didn't even know ‘lettering’ was an actual job that you could do. Back in the day, Jessica was a jack-of-all-trades (like many of us), but she realised that You Get The Work You Show. She filled her portfolio with self-initiated lettering work, and was soon able to specialise professionally.</p>
<p>Art directors (and all clients, in fact) aren't blinkered and lacking in imagination. With so much talent out there, they can find someone with <em>exactly</em> the right experience and demonstrable expertise. If you think lettering is your strongest skill but stack your portfolio with e-commerce websites, guess what kind of work you're more likely to be hired for?</p>
<p>If you want to specialise in something, you have to get off the mat and <em>do</em> it. Don't wait for the “dream commission” to come walking through the door; get out there and chase it down. Jessica makes her own luck, and makes me think I might be able to make my own, too.</p>
<h2 id="07%3A-trent-walton" tabindex="-1">07: <a href="http://twitter.com/TrentWalton">Trent Walton</a></h2>
<p>There are only so many ways to present seriffed text on a light background, but somehow Trent manages to make <a href="http://trentwalton.com/">his blog</a> look nicer than everyone else's. His art-directed posts (each with their own custom styling) are a benchmark for bloggers everywhere.</p>
<p>Somewhat annoyingly, what he writes about is <em>also</em> several orders of magnitude better than anything I could manage. I don't know if he became so respected by writing well about “big picture” subjects or whether he writes well <em>because</em> he's so respected, but either way, respected he is.</p>
<p>His posts come fairly infrequently, but when they do they're invariably beautiful <em>and</em> interesting.</p>
<h2 id="08%3A-ben-schwarz" tabindex="-1">08: <a href="http://twitter.com/benschwarz">Ben Schwarz</a></h2>
<p>Task runners (like Grunt and Gulp) have revolutionised my workflow over the past year. Grunt trickled into my sphere of awareness over a period of months, much like SASS. SASS I adopted more by osmosis than through any deliberate choice, but I can pin-point my adoption of Grunt to one specific incident.</p>
<p>Ben has made a <a href="http://benschwarz.github.io/gallery-css/">fantastic CSS-only Slider</a>, and I bought his accompanying screencast tutorial. It might be the best £10 I’ve ever spent. The video explains his intricate SASS, but also walks through a Grunt installation and shows how useful it is.</p>
<p>Up to that point, I was aware of Grunt but didn’t understand it or see the point of it. To see someone as consummately professional as Ben using it and extolling its virtues sealed the deal. I’ve since switched from Grunt to Gulp, but using <em>any</em> task runner at all has been the real quantum leap for my workflow.</p>
<h2 id="09%3A-sean-johnson-%26-liz-elcoate" tabindex="-1">09: <a href="http://twitter.com/seanuk">Sean Johnson</a> & <a href="http://twitter.com/liz_e">Liz Elcoate</a></h2>
<p>Quitting a job to go freelance is a daunting proposition. Making the leap myself, I found comfort in the curious trend for freelance-web-folk-focused podcasts. There are plenty of great offerings out there, but by far the most relevant to my past situation was <em><a href="http://www.thefreelanceweb.com/">The Freelance Web</a></em>.</p>
<p>Liz and Sean aren't afraid to deep-dive into the details of running a web business; both the good parts and the bad. Their advice is useful <em>and</em> reassuring, and the fact that they talk about specifics makes it all the more valuable.</p>
<p>I've since moved from freelancing to an agency position, but I still enjoy <em>The Freelance Web</em>. The willingness to share experiences and knowledge is one of the best characteristics of the web community. It's one I find it hard to believe any other industry could match, and Liz and Sean are true embodiments of that generous spirit.</p>
<h2 id="10%3A-daryll-doyle" tabindex="-1">10: <a href="http://twitter.com/enshrined">Daryll Doyle</a></h2>
<p>On a day-to-day basis, by far my biggest inspiration is my team mate, Daryll. He's very much from the school of thought that says “if you're not moving forwards, you're moving backwards”.</p>
<p>Pretty much constantly, Daryll is showing me some new tool he's discovered, technique he's mastered, or framework he's learnt. His dedication to self-improvement is nothing short of inspiring.</p>
<hr/>
<p>I know a couple of the people on this list IRL, and a couple more I'd class as ‘digital acquaintances’, but most are just web personalities who's work, public speaking, or blogging has taught me something new, steered me when my course has wandered, opened my eyes to new ways of thinking, or just been inspiring in some more ineffable manner.</p>
<p>People like this are what makes the web such a fantastic industry to be a part of, and I owe them all a debt of gratitude.</p>Fullpage screenshots in Firefox2014-02-07T00:00:00Zhttps://tomhazledine.com/fullpage-screenshots-in-firefox/<p>Every now and then I need to take a screenshot of an entire webpage, generally to use in my <a href="https://tomhazledine.com/portfolio">portfolio</a>. Manually positioning the cursor for a <code>cmd shift 4</code> screenshot (on a Mac) feels a little imprecise for my tastes, and I want to capture the <em>whole</em> of a site, not just the area visible in the viewport.</p>
<p>All the browser extensions I tried turned out a little buggy. Areas of the page would be repeated, like a visual interpretation of a CD skipping, or fonts would render strangely, as if they'd been run through an IE8 simulator or something.</p>
<p>Finally I stumbled upon this neat little trick. Sadly it doesn't help me get girls or lose weight like all the “one weird trick” pop-up ads keep promising me, but it does mean I can take a screenshot of an entire webpage using one line of code.</p>
<h2 id="the-answer!" tabindex="-1">The answer!</h2>
<p>Navigate to the page you want to shoot and open up Firefox's Developer Tools command line using <code>shift f2</code> (Mac users might need to remember to press <code>fn</code> as well, if their keyboard rebinds <code>f2</code> to increase the monitor's brightness). Then all you need to do is type the following line:</p>
<pre><code class="language-bash">screenshot --fullpage YourFileNameHere
</code></pre>
<p>And voilà! A screenshot has been taken of the current page in its entirety – not just the visible area! If you're super lazy, you can even leave off the file name; Firefox will just use the current date and time to save the image under.</p>
<p>I found I needed to crop off a sliver of transparency on the right hand side when I opened the shots up in Photoshop, but that's a small price to pay for the time saved not having to stitch together various peepshow-views of a page.</p>
<h2 id="see-it-in-action" tabindex="-1">See it in action</h2>
<p>You can see some of the screenshots I took using this method over on my portfolio page. ~The ‘phone’ view of the Atlantic Brewery page is particularly long and thin – an image I'd have struggled to create using any other method.~</p>Futura for the win2014-02-01T00:00:00Zhttps://tomhazledine.com/futura-for-the-win/<p>Picking the right typeface for this site took a long time, and my final choice wasn’t one I was expecting. Ask 3-months-ago-me if I'd set a website entirely in Futura and I'd have laughed at you.<sup class="footnote-ref"><a href="#fn1" id="fnref1">[1]</a></sup></p>
<figure class="figure--left"><img src="/images/articles/futuraDropcap.png" alt="" class="figure--left"/><figcaption>How the dropcap looked when this post was written</figcaption></figure>
<p>My initial ‘Contemporary Drop Cap’ concept was much more florid and overtly ornamental than what you see today (as of April 2014). I burnt through many drafts experimenting with ornate serif fonts before I finally remembered that what I was trying to build was a <em>portfolio</em>; a neutral backdrop for my designs, not something that would distract from the work on show. In short, I needed a sans.</p>
<h2 id="neutral-doesn't-mean-flavourless." tabindex="-1">Neutral doesn't mean flavourless.</h2>
<p>Even the most generic-seeming typefaces have their own distinct flavour, as my Helvetica-fuelled “Hey look! I just discovered Swiss Design” phase will attest (now commonly known as my “God-awful Superdry Period”).</p>
<p>I've developed a deep and abiding love for a perfectly circular “o”, so when choosing a sans I normally start with the geometrics. I'm particularly fond of the classically-British ‘modern’ fonts of <a href="http://en.wikipedia.org/wiki/Edward_Johnston">Edward Johnston</a> and the saucy <a href="http://en.wikipedia.org/wiki/Eric_Gill">Eric Gill</a>, but I've used them far too often already. If I used them in my portfolio site as well, I'd start to look like a one-trick pony.</p>
<p>H&FJ<a href="http://qz.com/167993/frere-jones-is-suing-hoefler-for-his-half-of-the-worlds-preeminent-digital-type-foundry/">(RIP)</a>'s Gotham looked lovely, but I can’t help but feel it’s on fashion's downward slope now. Far too many people are using it without thinking, and of course it carries a certain politicised air with it now, too (thanks, Obama!). So keeping with the geometric “o” theme, the next obvious choice was Futura.</p>
<h2 id="retro-futurism" tabindex="-1">Retro futurism</h2>
<p>I'd like to say there is some elaborate over-arching theoretical underpinning to my choice of Futura, but the reality is more prosaic. Sure I could waffle on about the retro-futurist stylings of the “W”s and “M”s, and how they fit into the dialectics of 20th Century design praxis, but in truth it was simply next on the list of “things to try”.</p>
<p>Once I saw my generously-margined headings set in Futura Bold I was smitten. Couple that with the not-too-shouty elegance of the all-caps italic, and I'd found the lynchpin for my design.</p>
<h2 id="flawed-futura" tabindex="-1">Flawed Futura</h2>
<p>Unlike many others, I actually quite like Futura's much-maligned question mark, for all it's eccentric meanderings. There are, though, some very visible problems with the ParaType cut of the font (the one I use via <a href="https://typekit.com/fonts/futura-pt">Typekit</a>), and quite possibly with the typeface as a whole, too.</p>
<p>The lowercase “J” looks stylish and bizarre in equal measure, and the semicolon is oddly top-heavy. In body copy the x-height is a tad lower than I find comfortable and the letter spacing after an apostrophe is massive. Not an issue in a programme that lets you kern, but an ugly cross to bear on the web.</p>
<p>In fact, all the apostrophes and quotation marks look the same. It’s all well and good training yourself to press <code>alt shift ]</code> every time you want an apostrophe, but Futura renders such efforts irrelevant. How many different characters can you count in the following sentence?</p>
<figure><img src="/images/articles/futuraGlyphsSmall.png" alt=""/><figcaption>Futura quotes etc. - small</figcaption></figure>
<p>Tough, eh?! It's a little easier at larger sizes:</p>
<figure><img src="/images/articles/futuraGlyphsLarge.png" alt=""/><figcaption>Futura quotes etc. - large</figcaption></figure>
<p>There are actually <em>seven</em> discrete glyphs there. There <em>are</em> differences, which at large sizes are delightfully subtle and nuanced, but at anything less than 72pt they all blur into one...</p>
<h2 id="a-designer's-website-is-like-a-builder's-house%3B-always-the-last-to-be-finished." tabindex="-1">A designer's website is like a builder's house; always the last to be finished.</h2>
<p>This blog-cum-portfolio site of mine has been through a seemingly infinite number of variations. What you see in April 2014 is only the third full ‘theme’ to make it all the way into code and deployment, but if you count Illustrator mockups then the number of iterations trebles. Count paper sketches – even limiting yourself just to fully drawn out and wire-framed designs – and the total skyrockets.</p>
<p>Only time will tell how long this current designs sticks around for, but it already feels like this version is a more substantial website than any of my previous attempts. Picking Futura and sticking with it, despite the issues and imperfections, has meant I've been able to turn my attention to the finer details: the little moments of interaction design, the negative space, and above all the <em>content</em>.</p>
<hr class="footnotes-sep"/>
<section class="footnotes">
<ol class="footnotes-list">
<li id="fn1" class="footnote-item"><p>This post was written in 2014. As you've no doubt already noticed, I've moved on from Futura as the typeface for this site. <a href="#fnref1" class="footnote-backref">↩︎</a></p>
</li>
</ol>
</section>Simple is hard2013-09-26T00:00:00Zhttps://tomhazledine.com/simple-is-hard/<p>“Simplify, simplfy!” we're often told. Your service isn't appealing to your customers? The answer's simple: simplify! Your song isn’t connecting with your fanbase? Simplify! You aren’t shifting as many units as Apple? Simplify! It’s an easy mantra to mock, and even easier to swallow hook, line, and sinker.</p>
<p>What we need to do is distinguish between ‘simple’ and ‘simplistic’. It's fairly obvious that content suffers when overrun by crowded, cluttered, un-navigable interfaces. If what you’re saying has value, you don’t need the kitchen sink to help you say it. But what do you need? And how much can you afford to throw out?</p>
<h2 id="only-keep-what-you-need." tabindex="-1">Only keep what you need.</h2>
<p>Deleting everything that you <em>don't</em> need only works when you know for sure what you <em>do</em> need. It's tempting to think of simplifying a design as merely stripping away layers of visual ornamentation, but true simplicity comes more from the <em>user experience</em> than the look of the thing. Hiding an unsightly menu behind a <a href="http://mobile.smashingmagazine.com/2012/10/08/the-semantic-responsive-design-navicon/">hamburger icon</a> is an easy way to de-clutter an app’s interface, for example, but does that count as simplification? All that achieves is forcing the user to make an extra click when they want to navigate somewhere. Even the <a href="http://100archive.com/now/doing-the-hard-work-to-make-it-simple">Government's Digital team agree</a> that this is both important <em>and</em> hard (and the new <a href="http://www.gov.uk">gov.uk</a> design is really impressive from a usability perspective).</p>
<p>Clearing away useless clutter is generally a good tactic, but there's a distinct difference between <em>simplifying</em> a product and merely making it's function more obtuse. Is one button with five functions really better than five buttons that each do one task? Is an aesthetically-pure minimalist interface really ideal if you then need to teach every new user how it works?</p>
<p>As with any task worth doing, there are many levels of nuance to simplification. It takes judgement; an assessment of what functions are important to your product, your app, your idea. Strip away anything extraneous and put anything the user needs front-and-centre, but don’t confuse the two or you’ll live to regret it.</p>
<h2 id="this-website-is-all-about-words." tabindex="-1">This website is all about words.</h2>
<p>The page you’re reading now went through many iterations before settling on the look you see today. Early drafts were swamped with full-page background images and clever CSS3 animation effects, but no matter how fancy I got with the coding these early designs never felt quite right. Then I stumbled upon <a href="http://justinjackson.ca/words.html">Justin Jackson's inspired ‘This Is A Web Page’ article</a> and was reminded that what was important for this site was the words themselves. It goes deeper than that, even: what's important to this site is the <em>content</em>. It could be read aloud by a screen-reader, translated into sign-language, even transmitted telepathically for all I care. As long as the message can be absorbed, this page will have done its job.</p>
<p>What I was doing with my early designs was trying to build a ‘great website’, when what I should really have been focusing on was building a great content delivery system. It seems video is the <em>de rigueur</em> medium for getting messages across quickly, and for a while I did toy with the idea of making this page into a vlog, but ultimately words are purer. Video is harder to create than written content, and would skew the message with all the baggage brought along by seeing my face (beautiful) or hearing my accent (perfectly inflected Queen's English). With the written word I have more chance of capturing the ‘white heat of inspiration’ and of being able to communicate my thoughts clearly.</p>
<h2 id="why-use-any-styling-at-all%3F" tabindex="-1">Why use any styling at all?</h2>
<p>So why not go all-out and strip out all the styling from this page completely? Properly written semantic HTML is nothing <em>but</em> content, so surely stripping away everything else would be the most efficient way to get the message across? Well, quite frankly, the default styling applied by web browsers is <em>ugly</em>, and that's not as shallow a complaint as you might think. Concepts like line-height, line-length, leading & kerning each have a huge impact on the readability of text. To trust all those to the random whims of a browser is madness.</p>
<p>It’s often said that <a href="http://alistapart.com/article/on-web-typography#section3">we read best what we read most</a>, and my what my friends and I read most are <em>books</em>. Books are fantastically efficient, and the best ones are things of beauty that wrap their content in a nourishing blanket of aesthetic proportions and finely-wrought typefaces. Given how many books I own solely on the topic of type design and typography, I can’t stand idly by and leave my content at the mercy of the ‘web-safe’ default fonts. I have to believe that good typography and good design makes the reading experience better.</p>
<h2 id="simple-doesn%E2%80%99t-have-to-be-ugly." tabindex="-1">Simple doesn’t have to be ugly.</h2>
<p>It seems obvious, but websites <em>still</em> have a lot to learn from books. Just because we <em>can</em> swamp everything in technical wizardry doesn’t mean we <em>should</em>, but it equally doesn’t mean we should abandon design altogether. ‘Simple’ doesn’t (and <em>shouldn’t</em>) mean ‘ugly’, and the simple act of thinking about line-lengths and visual hierarchies can turn an indigestible splurge of HTML into something readable, and in the right hands even something beautiful.</p>
<p>Finding the right balance between visual simplicity, simplicity of interaction and a pleasing aesthetic is a real challenge, and I’ll be the first to admit I’ve still a long way to go. But at least now I’m pointing in the right direction; simple is important, simple is necessary, and simple is really h</p>A bullet-journal workflow2013-09-24T00:00:00Zhttps://tomhazledine.com/bullet-journal-workflow/<p>Making task lists can be fantastically useful, but it's all too easy to lose them. My paper lists get buried under reams of doodles and random notes, or lost in the depths of a full notebook that simply has <em>too much in it</em>. Even digital lists get lost, and I find I lack the discipline to open a mammoth text file every morning. Paper is much better for immediate reference, even if it is easy to miss-place.</p>
<h2 id="productivity-hacking" tabindex="-1">Productivity Hacking</h2>
<p>The quest for elegant ‘productivity hacks’ is both fantastically useful and fantastically counter-productive. You can waste days reading about how not to waste your time. Finally, however, it seems that all the hours I've spent reading about productivity-hacks weren't wasted after all; my current system finds me being <em>more</em> efficient and productive than I've ever been before. And at the hub of my new-found momentum lies my Bullet Journal.</p>
<h2 id="the-bullet-journal-concept" tabindex="-1">The Bullet Journal Concept</h2>
<p>Devised by inspired Brooklynite <a href="http://www.rydercarroll.com/">Ryder Carrol</a>, the <a href="http://www.bulletjournal.com/">Bullet Journal</a> is little more than a seemingly-simple method of writing out a ‘to-do’ list using a pen and a notebook. I've experimented with similar list-hierarchies in the past, with mixed results, and while this new one is elegant, it's not exactly revolutionary. But there's one aspect of the Bullet Journal system that I've found to be a real revelation: the way the list rolls-over from day-to-day and month-to-month.</p>
<h2 id="what-first-drew-me-to-the-bullet-journal-was-the-systematized-nature-of-its-bullet-point-organization%2C-but-what's-kept-me-using-it-is-the-way-it-stays-relevant." tabindex="-1">What first drew me to the Bullet Journal was the systematized nature of its bullet-point organization, but what's <em>kept</em> me using it is the way it stays relevant.</h2>
<dl>
<dt>Simplicity.</dt>
<dd>I love simplicity, and the system itself is fairly straightforward – each list item is marked with either a square, circle or mid-dot. The square boxes denote tasks, circles events, and dots are standard notes (things to remember, but not explicitly actionable). At the start of every month I draw up a list of long-term goals, and then each morning I set out my daily tasks. I number the pages as I go, and keep an index at the front of the notebook. This makes assessing progress on long-term goals a piece of cake.</dd>
<dt>There's only ever one active list.</dt>
<dd>At the start of every day I write out a new daily list. Any items unchecked from the day before are either struck-through (which denotes they've become irrelevant) or marked with an arrow and copied into the new daily list. This way you never have to refer back to old lists hidden in the depths of the notebook, and it's painfully obvious when a task is being continually put-off (which forces you to act: either do it or mark it as irrelevant).</dd>
<dt>There's still long-term structure.</dt>
<dd>While there's only the one active list for you to deal with at any one time, the monthly lists are useful for reference, making it harder for day-to-day tasks to swamp your ‘big picture’ aims.</dd>
<dt>It's flexible.</dt>
<dd>The Bullet Journal system is loose enough that it never gets in the way of my day-to-day workflow. If i need a date-agnostic list on a specific topic, I just make it and add the page number to the index. Little additions like this make the whole process expandable and fairly all-encompassing, and the day-to-day running of the list is never disturbed by all these additions.</dd>
<dt>It's really easy.</dt>
<dd>Keeping the journal up to date sounds like rather an onerous task, what with all the indexing, page-numbering, and three-tiered bullet system. In reality, keeping on top of all my lists is no hassle at all. Updating the list takes about 10 minutes every morning, and up to half-an-hour on the first day of every month (as I update my calendar, and re-set my long-term goals).</dd>
</dl>
<figure><img src="/images/articles/journal.jpg" alt=""/><figcaption>My Bullet Journal</figcaption></figure>
<h2 id="the-journal-isn't-solely-responsible-for-my-productivity-upswing%2C-but-it's-definitely-helped." tabindex="-1">The journal isn't <em>solely</em> responsible for my productivity upswing, but it's definitely helped.</h2>
<p>By way of a disclaimer, making the switch to a Bullet Journal workflow wasn't the only change I've made recently. A few months ago I quit my day-job and struck out on my own; an intrepid freelancer <em>cum</em> entrepreneur bound for riches and glory! So now that any-and-all money that I make is directly proportional to how much work I do, it'll come as no surprise that I'm working harder and longer than ever before (and would be with or without the Bullet Journal).</p>
<p>But using the <a href="http://www.bulletjournal.com/">Bullet Journal</a> <em>has</em> undeniably helped my workflow. My days now have structure, and (most importantly) I'm now much less likely to overlook an important task.</p>