localghost Sophie builds fun things out of HTML, CSS & JavaScript, and writes blog posts about tech and mental health. 2026-02-08T00:00:00Z https://localghost.dev Sophie Koonin [email protected] Stop generating, start thinking 2026-02-08T00:00:00Z https://localghost.dev/blog/stop-generating-start-thinking/ <p>Throughout my career, I feel like I’ve done a pretty decent job of staying top of new developments in the industry: attending conferences, following (and later befriending!) some of the very smart people writing the specs, being the one sharing news on Slack about exciting new features of CSS or JS with my colleagues. The joys of working on an internal tool where you only need to worry about latest Chrome, and playing with anchor positioning in a production app while it’s still experimental!</p> <p>It’s very unsettling, then, to find myself feeling like I’m in danger of being left behind - like I’m missing something. As much as I don’t like it, so many people have started going so hard on LLM-generated code in a way that I just can’t wrap my head around.</p> <p>I’ve been using Copilot - and more recently Claude - as a sort of <a href="https://www.theguardian.com/us-news/ng-interactive/2026/jan/18/tech-ai-bubble-burst-reverse-centaur">“spicy autocomplete”</a> and occasional debugging assistant for some time, but any time I try to get it to do anything remotely clever, it completely shits the bed. Don’t get me wrong, I know that a large part of this is me holding it wrong, but I find it hard to justify the value of investing so much of my time perfecting the art of asking a machine to write what I could do perfectly well in less time than it takes to hone the prompt.</p> <p>You’ve got to give it enough context - but not too much or it gets overloaded. You’re supposed to craft lengthy prompts that massage the AI assistant’s apparently fragile ego by telling it “you are an expert in distributed systems” as if it were an insecure, mediocre software developer.</p> <p>Or I could just write the damn code in less time than all of this takes to get working.</p> <p>As I see more and more people generating code instead of writing it, I find myself wondering why engineers are so ready and willing to do away with one of the good bits of our jobs (coding) and leave themselves with the boring bit (reviews).</p> <p>Perhaps people enjoy writing roleplay instructions for computers, I don’t know. But I find it dangerous that people will willingly - and proudly - pump their products full of generated code.</p> <p>I’ll share a couple of the arguments I’ve encountered when I’ve expressed concern about this.</p> <h2 id="this-is-the-industrial-revolution-of-our-time-it-s-like-mechanisation-all-over-again" tabindex="-1">“This is the Industrial Revolution of our time! It’s like mechanisation all over again.”</h2> <p>Yes, this is true in many ways.</p> <p>Firstly, when you consider how much the Industrial Revolution <a href="https://www.oerproject.com/OER-Materials/OER-Media/HTML-Articles/Climate/Unit1/The-Industrial-Revolution-and-Climate-Change">contributed to climate change</a>, and look at the <a href="https://www.iea.org/reports/energy-and-ai/energy-demand-from-ai">energy consumption</a> of the data centres powering AI software, it’s easy to see parallels there. Granted, not all of this electricity is fossil-fuel-powered, so that’s some improvement on the Industrial Revolution, but we’re still wasting enormous amounts of resources generating pictures of <a href="https://www.businessinsider.com/meta-facebook-ban-ai-slop-images-shrimp-jesus-why-2024-6">shrimp Jesus</a>.</p> <p>Mechanisation made goods cheaper and more widely available, but at the cost of quality: it’s been a race to the bottom since the late 19th century and now we have websites like SHEIN where you can buy a highly flammable pair of trousers for less than a cup of coffee. Mechanisation led to a decline in skilled labour, made worse by companies gradually offshoring their factories to less economically developed countries where they could take advantage of poorly-paid workers with fewer rights, and make even more money.</p> <p>Generated code is rather a lot like fast fashion: it looks all right at first glance but it doesn’t hold up over time, and when you look closer it’s full of holes. Just like fast fashion, it’s often <a href="https://www.sundaypost.com/fp/our-designer-fear-small-firms-alarm-at-impact-of-high-street-copy-cats/">ripped off other people’s designs</a>. And it’s a scourge on the environment.</p> <p>But there’s a key difference. Mechanisation involved replacing human effort in the manufacturing processes with machinery that could do the same job. It’s the equivalent of a codemod or a script that generates boilerplate code. The key thing is that it <em>produces the same results each time</em>. And if something went wrong, humans would be able to peer inside the machine and figure out what went wrong.</p> <p>LLM output is <strong>non-deterministic</strong>, and the inner workings opaque. There’s no utility in a mechanised process that spits out something different every time, often peppered with hallucinations.</p> <h2 id="ll-ms-are-just-another-layer-of-abstraction-like-higher-level-programming-languages-were-to-assembly" tabindex="-1">“LLMs are just another layer of abstraction, like higher level programming languages were to assembly.”</h2> <p>It’s true that writing Java or Go means I never had to bother learning assembly. The closest I get to anything resembling assembly is knitting patterns.</p> <p>The way that we write software has evolved in terms of what we need to think about (depending on your language of choice): I don't have to think about garbage collection or memory allocation because the runtime does it for me. But I do still have to think about writing efficient code that makes sense architecturally in the wider context of our existing systems. I have to think about how the software I'm building will affect critical paths, and reason about maintainability versus speed of delivery. When building for the web, we have to think about browser support, accessibility, security, performance.</p> <p>Where I've seen LLMs do the most damage is where engineers outsource the <em>thinking</em> that should go into software development. LLMs can't reason about what the system architecture should be because <em>they cannot reason</em>. They do not think. So if we're not thinking and they're not thinking that means nobody is thinking. Nothing good can come from software nobody has thought about.</p> <p>In the wake of the <a href="https://en.wikipedia.org/wiki/British_Post_Office_scandal">Horizon scandal</a>, where innocent Post Office staff went to prison because of bugs in Post Office software that led management to think they’d been stealing money, we need to be thinking about our software more than ever: we need <em>accountability</em> in our software.</p> <p>Thirteen people killed themselves as a direct result of those bugs in that Post Office software, by the way.</p> <h3 id="our-terrible-code-is-the-problem" tabindex="-1">Our terrible code is the problem</h3> <p>But, you may argue, human developers today write inaccessible, unperformant, JavaScript-heavy code! What's the difference?</p> <p>Yes, <em>exactly</em> (or should I say “You’re absolutely right”?). LLMs are trained (without our explicit consent) on all our shitty code, and we've taught them that that's what they should be outputting. They are doomed to repeat humans’ mistakes, then be trained on the shitty reconstituted mistakes made by other LLMs in what’s (brilliantly) been called <a href="https://maggieappleton.com/ai-dark-forest#2-be-original-critical-and-sophisticated">human centipede epistemology</a>. We don't write good enough code as humans to deserve something that writes the same stuff faster.</p> <p>And if you think we’ve done all right so far, we haven't: just ask anyone who uses assistive technology, or lives in a country with terrible Internet connection (or tries to get online on mobile data in any UK city, to be honest). Ask anyone who's being racially discriminated against by facial recognition software or even a hand dryer. Ask the Post Office staff.</p> <p>Instead of wanting to learn and improve as humans, and build better software, we’ve outsourced our mistakes to an unthinking algorithm.</p> <h3 id="four-eyes-good-two-eyes-bad" tabindex="-1">Four eyes good, two eyes bad</h3> <p>Jessica Rose and Eda Eren gave a <a href="https://2025.ffconf.org/jessica-eda">brilliant talk</a> at FFConf last year about the danger of AI coding assistants making us lose our skills. There was one slide in particular that stood out to me:</p> <figure><picture><source type="image/webp" srcset="https://localghost.dev/img/xpP6-QQSQg-280.webp 280w, https://localghost.dev/img/xpP6-QQSQg-640.webp 640w, https://localghost.dev/img/xpP6-QQSQg-960.webp 960w" sizes="auto"><img loading="lazy" decoding="async" src="https://localghost.dev/img/xpP6-QQSQg-280.jpeg" alt="Jess and Eda on stage at FFConf in front of a slide that says &quot;Code you did not write is code you do not understand. You cannot maintain code you do not understand.&quot;" width="960" height="720" srcset="https://localghost.dev/img/xpP6-QQSQg-280.jpeg 280w, https://localghost.dev/img/xpP6-QQSQg-640.jpeg 640w, https://localghost.dev/img/xpP6-QQSQg-960.jpeg 960w" sizes="auto"></picture></figure> <p>The difference between reviewing a PR written by human and one by an LLM is that there's a certain amount of trust in a PR by a colleague, especially one that I know. The PR has been reasoned about: someone has thought about this code. There are exceptions to every rule, yes: but I'd expect manager intervention for somebody constantly raising bad PRs.</p> <p>Open source maintainers will tell you about the deluge of poor quality generated PRs they're seeing nowadays. As a contributor to any repository, you are accountable for the code you commit, even if it was generated by an LLM. The reviewer also holds some accountability, but you’ve still got two pairs of eyes on the change.</p> <p>I’ve seen social media posts from companies showing off that they’re using e.g. Claude to generate PRs for small changes by just chatting to the agent on Slack. Claude auto-generates the code, then creates the PR. At that point accountability sits solely with the reviewer. Unless you set up particularly strict rules, one person can ask Claude to do something and then approve that PR: we’ve lost one of those pairs of eyes, and there's less shared context in the team as a result.</p> <p>Reviewing PR isn't just about checking for bugs: it’s about sharing understanding of the code and the changes. Many companies don't do PRs at all and commit directly to the main branch, but the only way I've personally seen that work consistently at scale is if engineers are pairing constantly. That way you still have shared context about changes going in.</p> <h2 id="i-m-not-anti-progress-i-m-anti-hype" tabindex="-1">I'm not anti-progress, I'm anti-hype</h2> <p>I think it’s important to highlight at this stage that I am not, in fact, “anti-LLM”. I’m anti-the branding of it as “artificial intelligence”, because it’s not <em>intelligent</em>. It’s a form of machine learning. “Generative AI” is just a very good Markov chain that people expect far too much from.</p> <p>I don’t even begrudge people using generative AI to generate prototypes. If you need to just quickly chuck together a wireframe or an interactive demo, it makes a lot of sense. My worry is more around people thinking they can “vibe code” their way to production-ready software, or hand off the actual thinking behind the coding.</p> <p>Mikayla Maki had a <a href="https://zed.dev/blog/on-programming-with-agents">particularly good take</a> on working with agents: keep the human in the loop, treat them like an external contributor you don’t trust. Only use agents for tasks you already know how to do, because it’s vital that you understand it.</p> <p>I will continue using my spicy autocomplete, but I’m not outsourcing my thinking any time soon. Stop generating, start understanding, and remember what we enjoyed about doing this in the first place.</p> 2025: The year in lists 2025-12-30T00:00:00Z https://localghost.dev/blog/2025-the-year-in-lists/ <p>Skip to bits you care about:</p> <ul> <li><a href="https://localghost.dev/blog/2025-the-year-in-lists/#the-year-in">The year in...</a> <ul> <li><a href="https://localghost.dev/blog/2025-the-year-in-lists/#furry-friends">...furry friends</a></li> <li><a href="https://localghost.dev/blog/2025-the-year-in-lists/#retreating-into-my-cave">...retreating into my cave</a></li> <li><a href="https://localghost.dev/blog/2025-the-year-in-lists/#conferences">...conferences</a></li> <li><a href="https://localghost.dev/blog/2025-the-year-in-lists/#gardening">...gardening</a></li> <li><a href="https://localghost.dev/blog/2025-the-year-in-lists/#books">...books</a></li> <li><a href="https://localghost.dev/blog/2025-the-year-in-lists/#music">...music</a></li> <li><a href="https://localghost.dev/blog/2025-the-year-in-lists/#video-games">...video games</a></li> <li><a href="https://localghost.dev/blog/2025-the-year-in-lists/#blog-posts">...blog posts</a></li> </ul> </li> </ul> <h2 id="the-year-in" tabindex="-1">The year in...</h2> <h3 id="furry-friends" tabindex="-1">...furry friends</h3> <p>We <a href="https://localghost.dev/blog/touching-grass-and-shrubs-and-flowers-and-dog/">got a dog</a>! She's both the most wonderful and most annoying thing I've ever encountered. It's hard work and she needs a lot of training, but she's come a long way in the time we've had her. 2026 will bring more training sessions, and hopefully more success. As I write this she's befriended the people at the table next to me in the café, and doing her best poor orphan impression in the hopes of a bite of their sausage sandwiches.</p> <h3 id="retreating-into-my-cave" tabindex="-1">...retreating into my cave</h3> <p>I took a step back from social media in the latter half of the year, not for any particular reason, but it just wasn't making me happy. I'm keeping up with people's RSS feeds, but probably missing out on what folks are up to by not checking Bluesky/Mastodon. I still find myself picking up my phone absent-mindedly to look at things, realising I don't want to look at social media, and putting it down again.</p> <p>I'd hoped that being on social media less would lead to more blog posts, but it didn't; most of the time, the thought of writing a post felt like work, so I chose to just stay offline instead.</p> <h3 id="conferences" tabindex="-1">...conferences</h3> <p>As predicted I did indeed do fewer conferences this year, and I managed to use the migrations talk for all of them. I think that one's run its course now, so my conference visits in 2026 are much more likely to involve being in the audience!</p> <ul> <li>State of the Browser was in March this year, and it was great as always. It was great to finally meet <a href="https://sarajoy.dev/">Sara Joy</a>!</li> <li>I spoke at QCon London in April, which was a new experience for me - it was absolutely massive, five or six tracks, which I found completely overwhelming. There were some great talks and I enjoyed the web track but that size conference just isn't for me. I like the small ones where you can chat to people in the break without it feeling transactional.</li> <li>I travelled to Cluj in Romania in May to speak at <a href="https://jsheroes.io">JSHeroes</a>. Having heard lots of good things about it, I can confirm it's great vibes and the whole crew were wonderful people. I saw conference friends, made some great new ones, and would thoroughly recommend the conference. What an excellent community!</li> <li>Speaking of wonderful people, so were the crew of <a href="https://nordicjs.com/2025">NordicJS</a> in Stockholm in October. It was an absolute pleasure to speak there, the talks were all absolutely fascinating, and the festival vibes reminded me of JSConf EU. I took part in the Code in the Dark challenge at the afterparty, and properly crashed and burned.</li> <li>As usual I went back to StaffPlus which is now LDX3, and it was pretty good - merging the conferences into one big three-track event meant that sometimes I couldn't get into the talks I wanted to see, which was a shame. I much prefer single-track conferences. That said, I saw some great talks and came away with a ton of ideas for tackling tech debt and monitoring website/repo health.</li> <li>In Nov I went back to <a href="https://ffconf.org">FFConf</a> for the first time in a few years, and I remembered why I loved it so much. What an inspiring and thought-provoking conference.</li> </ul> <p>Next year you bet I'll be at the last-ever (sob) <a href="https://heypresents.com/conferences/2026">All Day Hey</a> in May, and I'm hoping to go back to Amsterdam for CSS Day in June. There's also State of the Browser in March, which I never miss!</p> <h3 id="gardening" tabindex="-1">...gardening</h3> <p>I took my second sabbatical in May, and spent the time designing my front garden. A few people have asked me about it recently, and I'm pleased to report it's taking shape, but it took a while to get started!</p> <p>May is actually a terrible time to redesign the garden, as really you want to get plants in the garden in or before spring so the roots can establish. Summer was so dry this year that the few plants I’d put in really weren’t very happy, plus the ground baked solid and I couldn’t dig it until mid-November.</p> <p>It rained for what seemed like three weeks straight in November, which was a blessing for the garden (and a curse for everyone else!) and meant the ground softened up enough to get the beds dug in. We bought a cubic metre of compost and distributed it across the beds, and I planted the first lot of hardy perennials and shrubs, which will just hang out until they start growing again in spring.</p> <p>I also bought a bare root pear tree which I’ll plant in the spring when it starts to warm up.</p> <h3 id="books" tabindex="-1">...books</h3> <p>This year I moved on from the “disproportionately disadvantaged female heroine turns out to be omnipotent god” genre, and read some other books I enjoyed more:</p> <ul> <li><a href="https://www.raynayler.net/the-mountain-in-the-sea.html">The Mountain in the Sea</a> by Ray Nayler: creepy sentient killer octopuses and secret research by shady tech corporations!</li> <li><a href="https://www.raynayler.net/where-the-axe-is-buried.html">Where the Axe is Buried</a> by Ray Nayler: I enjoyed the previous book so much I read this one immediately afterwards. The authoritarian president of a Federation that is Definitely Not Russia Wink Wink keeps himself alive by downloading his consciousness into younger bodies, and AI Prime Ministers reign supreme.</li> <li><a href="https://akcaggiano.com/villainsandvirtues/">Villains and Virtues</a> series by A.K. Caggiano: a fantasy trilogy following two very likeable characters, one of whom is a demon spawn on a quest to fulfil a dark prophecy. Don't be put off by the cover art, which makes it look like fanfiction: these are really well written.</li> <li><a href="https://www.jdevansbooks.com/">Mages of the Wheel</a> series by J.D. Evans: the Sultana of a magical kingdom embarks on a quest to unite mages of all the houses, and end the war with what is effectively the Roman Empire. While they are at their core romance books, there's plenty of gripping story and character development in a way that didn't make it feel like fluff. Each book in the series follows a different character.</li> <li><a href="https://hodderscape.co.uk/products/the-raven-scholar">The Raven Scholar</a> by Antonia Hodgson: a great fantasy read about a contest to replace an emperor and a woman who unravels a dark secret at the heart of it.</li> <li><a href="https://thebookerprizes.com/the-booker-library/books/reservoir-bitches">Reservoir Bitches</a> by Dahlia de la Cerda: a book of short stories about women in Mexico. Some are quite graphic (mega content warning for the first story which has a very graphic depiction of a terminated pregnancy) and some are just plain depressing, but this was a really really good read.</li> <li><a href="https://dannybate.com/book/">Why Q Needs U</a> by Danny Bate: a fantastic linguistic deep-dive into the history of each letter of the English alphabet. A long-time etymology nerd, I really loved this book and can't recommend it enough. Did you know the letter &quot;a&quot; is derived from a pictogram of an ox's head?</li> <li><a href="https://shadycharacters.co.uk/books/shady-characters-the-book/">Shady Characters: Ampersands, Interrobangs and Other Typographical Curiosities</a> by Keith Houston: a fascinating history of some common, and not-so-common, punctuation marks and symbols.</li> <li><a href="https://www.waterstones.com/book/butter/asako-yuzuki/9780008511715">Butter</a> by Asako Yuzuki: worth reading for the descriptions of food alone. This book was everywhere this year, and for good reason.</li> <li><a href="https://www.penguin.co.uk/books/457728/the-safekeep-by-wouden-yael-van-der/9780241999776">The Safekeep</a> by Yael van der Wouden: nominated for the International Booker Prize, this was a fascinating read set in post-war Netherlands.</li> <li><a href="https://www.waterstones.com/book/not-the-end-of-the-world/hannah-ritchie/9781529931242">Not the End of the World</a> by Hannah Ritchie: A more optimistic view on how we can tackle climate change, backed up with actual statistics and science. It's not naïve about the challenges ahead by any means, but it does make it all seem a little less futile.</li> <li><a href="https://www.mcdbooks.com/books/moonbound">Moonbound</a> by Robin Sloan: I really enjoy Robin Sloan's sci-fi writing, and this was characteristically bizarre and great fun. Set in the far future eleven thousand years from now, a young boy escapes an evil wizard and learns more about the strange world around him.</li> </ul> <h3 id="music" tabindex="-1">...music</h3> <p>In the latter couple of months I’ve been making more of an effort to listen to actual albums, because I’ve had my favourites playlist on repeat for most of the year and I tend to listen to the same few songs. On repeat this year was the K-Pop Demon Hunters soundtrack, which goes SO HARD and was my most listened to this year by a long way.</p> <p>Some other great albums this year:</p> <ul> <li><a href="https://music.apple.com/gb/album/mayhem/1792666546">MAYHEM</a> by Lady Gaga was a stellar return to form and I loved every track</li> <li><a href="https://music.apple.com/gb/album/ego-death-at-a-bachelorette-party/1850011909">Ego Death at a Bachelorette Party</a> by Hayley Williams</li> <li><a href="https://music.apple.com/gb/album/stardew-valley-festival-of-seasons/1831635023">Stardew Valley Festival of Seasons</a> - I had the pleasure of going to see this live at the beginning of the year, and it was wonderful. Orchestral versions of the brilliant soundtrack to this brilliant game.</li> </ul> <h3 id="video-games" tabindex="-1">...video games</h3> <p>This year we bought a Switch 2, and I played a ton of <a href="https://store.steampowered.com/app/1145350/Hades_II/">Hades 2</a>! I'm actually getting pretty good at it. The soundtrack is great, too.</p> <p>I also played <a href="https://www.hollowknight.com/">Hollow Knight</a>for the first time, and beat the game even though at times I wanted to rage quit and throw the console across the room. I used to avoid soulslikes as I thought they were too hard, but it turns out I needed to approach the boss fights differently from the Borderlands-style games where you just spray-and-pray. It was an exercise in patience, and it ultimately paid off (plus it made Hades seem a lot more attainable). James played the sequel this year and I can safely say that I will not be going near it, having watched him play.</p> <p>I spent a LOT of time on <a href="https://www.blueprincegame.com/">Blue Prince</a>, which was exactly my brand of mystery/puzzle game.</p> <h3 id="blog-posts" tabindex="-1">...blog posts</h3> <p>Considerably fewer than last year, but they still seemed to resonate with folks, especially the one about AI slop!</p> <ul> <li><a href="https://localghost.dev/blog/i-repaired-my-steam-deck-and-it-was-fine-actually/">I repaired my Steam Deck and it was fine actually</a>: replacing the screen on a console that's designed to be DIYed.</li> <li><a href="https://localghost.dev/blog/my-month-of-rest-and-relaxation/">My month of rest and relaxation</a>: reflections from my first month's sabbatical from work.</li> <li><a href="https://localghost.dev/blog/the-blog-questions-challenge/">The blog questions challenge</a>: a fun little quiz doing the rounds earlier this year.</li> <li><a href="https://localghost.dev/blog/this-page-is-under-construction">This page is under construction: a love letter to the personal website</a></li> <li><a href="https://localghost.dev/blog/touching-grass-and-shrubs-and-flowers-and-dog/">Touching grass, and shrubs, and flowers, and dog</a>: designing my garden, and a new furry companion.</li> <li><a href="https://localghost.dev/blog/this-website-is-for-humans/">This website is for humans</a>: a diatribe against AI slop stealing people's work</li> </ul> <p>I also wrote a <a href="https://piccalil.li/blog/building-a-typed-fetch-in-typescript-with-conditional-types-and-infer/">guest post</a> for Piccalilli about conditional types in TypeScript.</p> <p>I feel like I have another post in me before the end of the year, but I also have a lot of games to play...</p> <h2 id="that-s-it-for-now" tabindex="-1">That’s it for now <!-- omit in toc --></h2> <p>As the Web Framework of Time pollutes the remainder of the year with the Unnecessary JavaScript of Eternity, it's time to say goodbye for 2025 and I will see you in the new year!</p> This website is for humans 2025-08-08T00:00:00Z https://localghost.dev/blog/this-website-is-for-humans/ <p>Walking past a bus stop yesterday I saw an advert for Google’s AI search. The person in the ad had pointed their phone’s camera at a bowl of ramen, and the AI result explained how to reproduce it at home.</p> <p>How does it know? Because it’s trained on all the ramen recipes that multiple recipe authors spent hours, weeks, <em>years</em> perfecting. Generative AI is a blender chewing up other people’s hard work, outputting a sad mush that kind of resembles what you’re looking for, but without any of the credibility or soul. Magic.</p> <p>I subscribe to a lot of recipe websites via RSS, and look forward to new posts from some of my favourites like <a href="https://smittenkitchen.com">Smitten Kitchen</a> and <a href="https://www.theguardian.com/profile/meera-sodha">Meera Sodha</a> because I know they’re going to be excellent. I trust that the recipe is tried and tested, and the result will be delicious. ChatGPT will give you an approximation of a recipe made up from the average of lots of recipes, but they lack the personality of each individual recipe, which will be slightly different to reflect the experiences and tastes of the author.</p> <p>There's a fair bit of talk about “<a href="https://www.theverge.com/24167865/google-zero-search-crash-housefresh-ai-overviews-traffic-data-audience">Google Zero</a>” at the moment: the day when website traffic referred from Google finally hits zero. If the AI search result tells you everything you need, why would you ever visit the actual website?</p> <p>Well, I want you to visit my website. I want you to read an article from a search result, and then discover the other things I’ve written, the other people I link to, and explore the weird themes I’ve got. I want some of you to read my article then ask me to speak at your conferences. Many folks rely on ad impressions to support the high-quality content they’re putting out for free.</p> <p>I write the content on this website for people, not robots. I’m sharing my opinions and experiences so that you might identify with them and learn from them. I’m writing about things I care about because I like sharing and I like teaching. I spend hours writing these posts and AI spends seconds summarising them.</p> <p>I'd much rather people read the whole thing, take it in, digest it and have opinions right back at me. I love it when people connect with what I’m writing (and sometimes they email me to tell me that, which is really delightful).</p> <p>I <em>don’t</em> write these posts for VC-funded LLMs to come along and gobble up and produce some shitty facsimile, or summarise what I’m saying with none of the nuance or context on someone else's website.</p> <p>This website is for humans, and LLMs are not welcome here.</p> Touching grass (and shrubs, and flowers, and dog) 2025-07-05T00:00:00Z https://localghost.dev/blog/touching-grass-and-shrubs-and-flowers-and-dog/ <p>I meant to post this in May, but then I went back to work after my final (sob) sabbatical, and I also lost a lot of time to Blue Prince at some point, so here we are in July.</p> <p>I thought I'd update with some medium-sized news in the form of a medium-sized dog:</p> <figure><picture><source type="image/webp" srcset="https://localghost.dev/img/BAze3B-Uwh-280.webp 280w, https://localghost.dev/img/BAze3B-Uwh-640.webp 640w, https://localghost.dev/img/BAze3B-Uwh-960.webp 960w" sizes="auto"><img loading="lazy" decoding="async" src="https://localghost.dev/img/BAze3B-Uwh-280.jpeg" alt="A honey-coloured staffy mix sits on the sofa with a cuddly toy aubergine/eggplant in her mouth" width="960" height="1280" srcset="https://localghost.dev/img/BAze3B-Uwh-280.jpeg 280w, https://localghost.dev/img/BAze3B-Uwh-640.jpeg 640w, https://localghost.dev/img/BAze3B-Uwh-960.jpeg 960w" sizes="auto"></picture></figure> <p>Her name is Penny. She's a staffy mix (and a rescue, so the rest is a mystery) and she's the cuddliest dog you've ever met. We've been working hard on lead training, as she is a terrible goblin on the lead and wants to chase anything small and furry, but then she makes up for it by doing things like this:</p> <figure><picture><source type="image/webp" srcset="https://localghost.dev/img/CwfGRERyCD-280.webp 280w, https://localghost.dev/img/CwfGRERyCD-640.webp 640w, https://localghost.dev/img/CwfGRERyCD-960.webp 960w" sizes="auto"><img loading="lazy" decoding="async" src="https://localghost.dev/img/CwfGRERyCD-280.jpeg" alt="A honey-coloured staffy snoozes on the sofa with her paw in her human's hand" width="960" height="1280" srcset="https://localghost.dev/img/CwfGRERyCD-280.jpeg 280w, https://localghost.dev/img/CwfGRERyCD-640.jpeg 640w, https://localghost.dev/img/CwfGRERyCD-960.jpeg 960w" sizes="auto"></picture></figure> <h2 id="designing-the-front-garden" tabindex="-1">Designing the front garden</h2> <p>I spent most of my May sabbatical gardening: it was finally time to tackle our massively overgrown front garden. My original plan was to get someone in to design the space (just a planting scheme, nothing hugely fancy) but the cheapest quote I got was £1500. So I decided I'd do it myself - how hard could it be? (the answer is very) - and spent some quality time with a (now rather chewed, thanks to Penny) copy of What Plant Where. I made a spreadsheet of all the plants that would work in our heavy clay soil, organised into whether they're shade-lovers (for the north-facing bed) or sun-lovers (for the south-facing bed). I even used a colour scheme generation tool (<a href="https://coolors.co">coolors<br> </a>) that I often use for web development to find colour combinations that worked for the beds.</p> <p>It's important to find a mix of plants that provide year-round interest, so you don't end up with a riot of colour in summer and a sad wasteland the rest of the year. So in my spreadsheet I also included what months the plants are interesting in and made sure to include a good number of evergreens. I've gone for a lot of plants that pollinators like: foxgloves, lavender, teasel.</p> <p>I ended up with something like this:</p> <figure><picture><source type="image/webp" srcset="https://localghost.dev/img/WEkggbt68R-280.webp 280w, https://localghost.dev/img/WEkggbt68R-640.webp 640w, https://localghost.dev/img/WEkggbt68R-960.webp 960w" sizes="auto"><img loading="lazy" decoding="async" src="https://localghost.dev/img/WEkggbt68R-280.jpeg" alt="A multicoloured spreadsheet with a list of plants organised by which bed they are suitable for, along with their height, spread, months of interest, foliage and flower colour." width="960" height="264" srcset="https://localghost.dev/img/WEkggbt68R-280.jpeg 280w, https://localghost.dev/img/WEkggbt68R-640.jpeg 640w, https://localghost.dev/img/WEkggbt68R-960.jpeg 960w" sizes="auto"></picture></figure> <p>On the shady side, I'm going for hostas and ferns to provide a bit of foliage; delicate white blooms that will really pop out against the green; and dots of blue and purple to provide some colour.</p> <p>On the sunny side we've got brighter colours: yellows, purples, oranges, pinks and even a bit of red.</p> <p>The next step was to produce a scale drawing of the plot. It's quite a uniquely shaped plot as we live on the corner of a road, so I used Google Maps' satellite view to get an overview of the shape. I measured each wall, and drafted it out on graph paper at a scale of 2cm=1m.</p> <p>I spent a while coming up with the shape of the lawn: I wanted big flowerbeds, but also a bit of grass in the middle. Curves were important to give a sense of movement and flow as opposed to the very constrained and formal feel of straight borders.</p> <p>Finally, it was time to add in the plants. Starting from the outside, I stuck the shrubs in first and then worked inwards, putting taller plants towards the back and making sure the design left plenty of room for plants to spread out. As a rule of thumb (so I have learned), you want to plant herbaceous plants in 3s or 5s, never on their own and not in even numbers because it looks weird.</p> <figure><picture><source type="image/webp" srcset="https://localghost.dev/img/hcmGTLnmas-280.webp 280w, https://localghost.dev/img/hcmGTLnmas-640.webp 640w, https://localghost.dev/img/hcmGTLnmas-960.webp 960w" sizes="auto"><img loading="lazy" decoding="async" src="https://localghost.dev/img/hcmGTLnmas-280.jpeg" alt="A top-down black and white hand-drawn design of a garden, with circles to represent where plants should go. The circles are annotated with the names of plants, such as Hydrangea Macrophylla &quot;Bluebird&quot;, Daphne bholua, Deutzia gracilis, Salvia nemorosa" width="960" height="720" srcset="https://localghost.dev/img/hcmGTLnmas-280.jpeg 280w, https://localghost.dev/img/hcmGTLnmas-640.jpeg 640w, https://localghost.dev/img/hcmGTLnmas-960.jpeg 960w" sizes="auto"></picture></figure> <h2 id="planting-it-up" tabindex="-1">Planting it up</h2> <p>It's going to take me several years to get the garden to where I want it to be, as there are still a few shrubs that need to come out, and a lot of bed space that needs to be cleared. It's the wrong time of year to be doing this really (autumn would be better, and the ground's rock solid in the heat). But I've officially done one corner, with the help of my mum and several bags of very strong-smelling manure.</p> <p>I've planted a deutzia and a daphne - two lovely pale-flowering shrubs, the daphne flowering in late winter/early spring - and surrounded them with some white erysimum (wallflowers). At the front of the border (by the lawn) we've got some brunnera macrophylla, a favourite of mine with these beautiful massive silver veined leaves and tiny blue forget-me-not flowers. The lovely thing about many perennials is that you can actually split them up into smaller plants, saving a bit of money as you don't need to buy so many. One brunnera became two, and two erysimums became three.</p> <h2 id="pelican-town-s-economy-is-safe-for-now" tabindex="-1">Pelican Town's economy is safe for now</h2> <p>I was looking forward to living my best Stardew Valley life this summer, growing fruit and veg in the back garden. In reality, the birds got all my strawberries (I didn't cover them over well enough) and the leaves of my potato plants have been absolutely devoured by tiny flies so god only knows whether I'll get any potatoes this year. I've successfully grown some chard and cavolo nero, though, and I have some very good cucumbers growing on a plant that was supposed to be a courgette. There are also early signs we might have a few raspberries this year (though literally only a few). At least I've learned things for next year!</p> This page is under construction 2025-02-22T00:00:00Z https://localghost.dev/blog/this-page-is-under-construction/ <p><em>This is an updated &amp; abridged version of the talk I gave at several conferences throughout 2022/23, including <a href="https://www.youtube.com/watch?v=2ZUqa-lTbnU">Beyond Tellerrand</a>, <a href="https://www.youtube.com/watch?v=H2Ux0hGQcs4">CSS Day</a> and <a href="https://www.youtube.com/watch?v=vGYm9VdfJ8s">FFConf</a>.</em></p> <p>If you take just one thing away from this article, I want it to be this: <strong>please build your own website</strong>. A little home on the independent web.</p> <p>A reflection of your personality in HTML and CSS (and a little bit of JS, as a treat). This could be a professional <a href="https://www.cassie.codes/">portfolio</a>, listing your accomplishments. It might be a <a href="https://ohhelloana.blog/">blog</a> where you <a href="https://rknight.me/">write</a> about <a href="https://sallylait.com/blog/">things</a> that <a href="https://www.miriamsuzanne.com/">matter</a> to you. It could even be something very <a href="http://endless.horse/">weird</a> and <a href="https://eelslap.com/">pointless</a> (even better) – I love a good <a href="https://theuselessweb.com/">single-joke website</a>. Ultimately, <strong>it's your space and you can do whatever you want with it</strong>.</p> <p>In the early days of the web, there were a lot of sites like this. People would build websites for their families, fansites for bands they liked, or just homepages they filled with random junk. It was a fun thing to do, and a great way to connect with people. They really don't make 'em like they used to.</p> <figure> <picture><source type="image/webp" srcset="https://localghost.dev/img/JjIz4h52gG-280.webp 280w, https://localghost.dev/img/JjIz4h52gG-640.webp 640w, https://localghost.dev/img/JjIz4h52gG-960.webp 960w" sizes="auto"><img loading="lazy" decoding="async" src="https://localghost.dev/img/JjIz4h52gG-280.jpeg" alt="A brightly coloured website that says 'Welcome to Tom & Sherry's Proud Grandparents page. The Proud Grandparents page was created to show pictures of our grandchildren to family and friends, and an occasional Web surfer. The grandkids, our pride and joy, and their parents have made us very proud. Okay, let's see the pictures!'" width="960" height="713" srcset="https://localghost.dev/img/JjIz4h52gG-280.jpeg 280w, https://localghost.dev/img/JjIz4h52gG-640.jpeg 640w, https://localghost.dev/img/JjIz4h52gG-960.jpeg 960w" sizes="auto"></picture> <figcaption><a href="https://geocities.restorativland.org/Heartland/Ridge/1217/">The Proud Grandparents Page</a> </figcaption></figure> <p>It feels like we've lost this decades-old art form; the individuality of design and the uniqueness of content you used to see on these webpages. The notion of experimenting with HTML and CSS without worrying about something looking weird or out of place. The beauty of a website built by a person, <em>because they wanted to</em>.</p> <h2 id="but-websites-are-for-business-right" tabindex="-1">But websites are for business... right?</h2> <p>Many of you reading this will probably get paid to build websites. I do, to an extent. And a lot of these websites will probably be very similar: marketing, e-commerce, ultimately something designed to make money either by selling something or advertising something. Maybe you've had to stick some tracking pixels in, do some a/b testing to see what variant converts better, and do some forbidden invocations to get Google Tag Manager working. The kind of web development that makes you question your life choices.</p> <p>And these sites all look <em>identical</em>. The same style of icons, black CTAs, the same pops of the same colours. I keep landing on websites and thinking I'm looking at <a href="https://vercel.com">Vercel</a>, and it's become this kind of bland VC-funded corporate identity that all startups have nowadays. And a lot of the content is similar too, because it ultimately comes down to what works for SEO. I gave this talk before Generative AI was quite so ubiquitous, and it's even worse now with so many websites full of absolute garbage content spewed out of ChatGPT, and nobody knows how Google ranks pages any more.</p> <p>These sites are providing a service, and are transactional in nature. There's always going to be a place on the internet for them – even if I wish they'd put a bit more effort into making their sites a bit more interesting-looking. I guess I'm asking: <em>where did the fun web go?</em></p> <p>I learned HTML back in 2000, and things were pretty different then*. I can't imagine how intimidating it must be to be a newcomer now with all of these frameworks and expectations. If you do get into web dev now you're probably doing it to make a career of it; you might do a bootcamp, or a course, some kind of formalised training. And if you are studying with a view to doing it professionally, you're much more likely to target what people are hiring for – whichever framework is so hot right now – skipping right over the basics.</p> <p>I've interviewed quite a few folks for web development jobs over the years, where it's clear they've only ever done React – and the requisite amount of JavaScript required to <em>do React</em> – and never got a good foundation in HTML or CSS that you need to build good quality, accessible, performant websites.</p> <p>* but 99% of that HTML would still work perfectly on the web today, thanks to glorious backwards compatibility! Isn’t that cool? (Provided it wasn’t mostly <code>&lt;marquee&gt;</code> tags, of course...)</p> <h2 id="but-i-still-have-fun-on-the-web" tabindex="-1">But I still have fun on the web!</h2> <p>Well, sure. But ultimately the websites we use for fun are also for business. Think about how you use the web today: chances are, many of you spend the majority of your time on a handful of websites and apps owned by big companies like Meta and Condé Nast.</p> <p>Under the guise of a free service, these companies are making money from data they're harvesting from us. &quot;We are the product&quot; may be a tired cliché at this point, but it's a cliché for a reason. They might not be explicitly selling it on, but they are 100% using your data to feed into advertising algorithms. We upload images, videos, run our social lives and even our businesses on these platforms. Instagram's terms of service give Meta carte blanche to reproduce, distribute or copy your images royalty-free.</p> <p>Meta makes an absolute killing on advertising that can reach highly specific target markets, and they wouldn't be able to do that without using your personal data and browsing habits to label your user account with things they think you like.</p> <p>Have you looked at your Instagram/Facebook settings lately, in the ad preferences section? It's quite illuminating what they decide to use to target you.</p> <p>Laura Kalbag gave <a href="https://ffconf.org/talks/privacy/">a great talk about digital privacy</a> at FFConf 2019, and it was this talk that spurred me on to ditch Gmail and set up a paid-for email account. I recommend giving it a watch.</p> <h2 id="sign-in-to-view-more" tabindex="-1">Sign in to view more</h2> <p>For many people, a social media account is the lowest-friction way to have an online presence. You don’t need any technical skills, it doesn’t cost you anything, and you’ve got a captive audience in the form of people on the social network.</p> <p>The flip side of this is, if anyone <em>doesn’t</em> have an account on the site they can’t see very much at all. My two local cafés both have an Instagram account and nothing else, and I don’t have Instagram any more so I can only see about two pictures on the web before the page asks me to sign in. Similarly, many small businesses only have a Facebook page which won’t show me many posts or pictures until I log in.</p> <figure><picture><source type="image/webp" srcset="https://localghost.dev/img/o7PeXzppXx-280.webp 280w, https://localghost.dev/img/o7PeXzppXx-640.webp 640w, https://localghost.dev/img/o7PeXzppXx-960.webp 960w" sizes="auto"><img loading="lazy" decoding="async" src="https://localghost.dev/img/o7PeXzppXx-280.jpeg" alt="A screenshot of a Georgian restaurant on Facebook. There is a translucent overlay on the screen preventing any interaction with the page, with a modal that says &quot;Sign in to Facebook&quot;." width="960" height="775" srcset="https://localghost.dev/img/o7PeXzppXx-280.jpeg 280w, https://localghost.dev/img/o7PeXzppXx-640.jpeg 640w, https://localghost.dev/img/o7PeXzppXx-960.jpeg 960w" sizes="auto"></picture></figure> <h2 id="unfollow" tabindex="-1">Unfollow</h2> <p>It took a billionaire narcissist taking over my favourite social network in 2022 to disrupt my very unhealthy relationship with social media. I was perma-glued to Twitter, workshopping tweets and thinking about how I could phrase things to get the maximum number of likes – addicted to the dopamine hits of likes and follows. You start to think: &quot;what can I post that people will respond positively to?&quot;. The more followers I had, the higher the bar for engagement got: I couldn't risk posting something <em>not funny</em> or <em>boring</em>. I didn't know these people, they didn't know me, and yet it was occupying far too much of my brain.</p> <p>And because you only have a set character limit, you find yourself pre-empting the backlash you might get. There's absolutely no room for nuance, so you have to think &quot;how could someone (often deliberately) misinterpret this?&quot;.</p> <figure><picture><source type="image/webp" srcset="https://localghost.dev/img/fj7rmVeFWb-280.webp 280w, https://localghost.dev/img/fj7rmVeFWb-640.webp 640w, https://localghost.dev/img/fj7rmVeFWb-960.webp 960w" sizes="auto"><img loading="lazy" decoding="async" src="https://localghost.dev/img/fj7rmVeFWb-280.jpeg" alt="A screenshot of a tweet by @haircut_hippie from Sept 16, 2021 that says &quot;there's an extraordinarily defensive style of writing where I can recognize if someone is probably active on Twitter or not by how often they include weird preemptive defenses of positions no sane person would ever take&quot;" width="960" height="514" srcset="https://localghost.dev/img/fj7rmVeFWb-280.jpeg 280w, https://localghost.dev/img/fj7rmVeFWb-640.jpeg 640w, https://localghost.dev/img/fj7rmVeFWb-960.jpeg 960w" sizes="auto"></picture></figure> <p>Well, the good news is there's no reply guys on your own website. You can write at length, ramble nonsensically, and people can choose to read it or not. It’s about putting things out on the internet for yourself.</p> <p>All of this content we’re writing on other people’s platforms is also stuck in that platform unless you request a data export of it, and even then it’s often in some weird proprietary format. During one of the big Twitter exoduses in 2022, the systems were under a lot of strain and it seemed like the export wasn’t going to arrive at all. What if you get banned for some reason, or locked out of your account – there’s nobody to talk to to appeal the decision, and your data might be lost forever. It won’t get archived by the Wayback Machine if it’s behind a login screen.</p> <p>I still dip in to Mastodon and Bluesky periodically, posting the odd silly thing or bit of news, but anything I really care about goes on here now, in cross-compatible Markdown format, archived on GitHub and elsewhere.</p> <p>You can be a creator anywhere on the internet these days, but there's only a small handful of places where you actually <em>own your own content</em>. Your own website is one of them.</p> <h2 id="internet-of-slop" tabindex="-1">Internet of Slop</h2> <p>When I originally wrote the Personal Websites talk in 2022, generative AI was something people were just starting to experiment with, mainly to make really stupid-looking generated images with DALL-E. Within a couple of years it's absolutely everywhere: we have a million identical &quot;AI&quot; startups popping up – many of whose products are just ChatGPT in a trenchcoat – and creators’ work is being ripped off left, right and centre. Silicon Valley is trying to invent AI solutions to problems we don't even have. Companies like LinkedIn are using our data and the content we post on their platforms to train their models, but what's worse, most are making it <em>opt-out</em>.</p> <p>Side note, imagine a model trained on the entirety of LinkedIn: talk about garbage-in-garbage-out.</p> <p>I’d like to point you towards Maggie Appleton’s excellent talk <a href="https://maggieappleton.com/forest-talk">The Expanding Dark Forest and Generative AI</a> which goes into this in a little more detail and offers some food for thought that may give you indigestion.</p> <blockquote> <p>[It] feels like we’re surrounded by content that doesn’t feel authentic and human. Lots of this content is authored by bots, marketing automation, and growth hackers pumping out generic clickbait with ulterior motives.</p> </blockquote> <p>People have already started putting little badges on their site that say “Made by a human”, and Maggie asks: is there a future in which we need to have some kind of &quot;reverse Turing test&quot; to prove our humanity on the internet?</p> <p>Well, shit. What do we do about all of this? The internet sounds like a pretty dire place right now. Should we just abandon ship and leave it to burn, let the AI models train on AI slop and spit out nonsensical word salad that humans don't even read?</p> <p>Of course not.</p> <h2 id="it-wasn-t-supposed-to-be-like-this" tabindex="-1">It wasn't supposed to be like this</h2> <p>The whole idea of the World Wide Web was that it was decentralised and independent. It was released as an open standard so that access could be as democratic as possible.</p> <p>Tim Berners-Lee gave an <a href="https://www.vanityfair.com/news/2018/07/the-man-who-created-the-world-wide-web-has-some-regrets">interview</a> in Vanity Fair in 2018 where he said:</p> <blockquote> <p>It was all based on there being no central authority that you had to go to to ask permission... The spirit there was very decentralized. The individual was incredibly empowered. That feeling of individual control, that empowerment, is something we’ve lost.</p> </blockquote> <p>We owe it to ourselves to bring that spirit back, and we can do that by carving out our own individual spaces on the web.</p> <h2 id="the-modern-web-is-for-consumers-not-creators" tabindex="-1">The modern web is for consumers, not creators</h2> <p>There are so many tools out there for building personal websites, HTML is wonderfully backwards-compatible, and there are a plethora of free web hosts out there. So <em>why aren't we doing it?</em></p> <p>Somewhere along the way, websites stopped being about the creators, and started being about the consumers: we don’t build websites for ourselves like we used to, we build them for the audiences we want.</p> <p>I see the personal website as being an antidote to the corporate, centralised web. Yeah, sure, it's probably hosted on someone else's computer – but it's a piece of the web that belongs to you. If your host goes down, you can just move it somewhere else, because it's just HTML.</p> <p>Sure, it's not going to fix democracy, or topple the online pillars of capitalism; but it's making a political statement nonetheless. It says &quot;I want to carve my own space on the web, away from the corporations&quot;. I think this is a radical act. It was when I originally said this in 2022, and I mean it even more today.</p> <h2 id="damn-it-i-m-in-where-do-i-sign-up" tabindex="-1">Damn it, I'm in! Where do I sign up?</h2> <p>I want to be clear, I'm not telling you to ditch social media completely, or stop building websites for business. There is still plenty of room on the web for commercial websites alongside the independent personal web. This is very much not a plea to go back to the old days of GeoCities, but rather a plea to bring the magic of the old days into the present day.</p> <p>I also don't want to be prescriptive about what this website should be. It's your space, and you can do with it whatever you want – whether that's a maximalist extravaganza, or plain text on a plain background. You might spent hours hand-crafting your HTML, or use a drag-and-drop builder. You may host it on someone else's platform, or on a box in your bedroom. All of these things are valid, as long as you build it <em>for you</em>.</p> <p>This website used to be just a blog and portfolio, but I've been continuously adding more and more things to it because I wanted to: pages about <a href="https://localghost.dev/keyboards">keyboards</a>, my favourite <a href="https://localghost.dev/games">games</a>, things I <a href="https://localghost.dev/uses">use</a>, weird <a href="https://localghost.dev/projects">projects</a> I've done. I've built multiple themes with some really silly features, but it's all done in a way that works without JS and respects people's motion preferences. I believe you can - and should - have a creator-focussed website which is still considerate towards the people reading it.</p> <p>You don't need to think about SEO or design trends... unless you want to. You could... list recipes you like, start a blog, write about tech things, share pictures, poems, little notes. Your space is an opportunity to be as weird as you want, to experiment and learn.</p> <p>For the nerds: try out the things behind experimental flags. Go to MDN and find the weirdest sounding API. Experiment in production, write bad code and ship it. If you screw it up, just revert. Undo. This site deploys in &lt;30 seconds, so nobody would ever know I messed it up (and I have, repeatedly). Drop in new features of CSS without worrying about whether your customers’ browsers support it; just make sure you lean on <a href="https://www.gov.uk/service-manual/technology/using-progressive-enhancement">progressive enhancement</a> so your page still works for everyone. As long as you're building the foundation of your site in regular old HTML, the content will show up regardless of the fancy stuff, and the weird stuff will be there for you (and anyone else who's got Chrome Canary).</p> <p>The tools are the same as they've ever been: HTML and CSS. It's still completely possible to write some HTML and CSS, stick it on a server and have a static webpage. It's so easy nowadays to get bogged down in build tools and frameworks. You can use them if you want, but it's not a requirement for web development even in 2025.</p> <p>Free hosting is better than it's ever been thanks to storage getting cheaper and cheaper. This website is hosted on <a href="https://neocities.org">Neocities</a>, which has a free tier with zero ads and 100% good vibes. It's aimed at anyone who wants to make a website, and they've got an HTML tutorial for beginners. You can upload static files, use the inline editor, or <a href="https://localghost.dev/blog/how-i-deploy-my-eleventy-site-to-neocities/">deploy automatically via CI</a> if you're a bit more technically-minded. A lot of the sites on Neocities are very nostalgic and evoke the late 90s and early 2000s: there’s a really nice community vibe there.</p> <p>I especially like their manifesto:</p> <blockquote> <p>We are tired of living in an online world where people are isolated from each other on boring, generic social networks that don't let us truly express ourselves. It's time we took back our personalities from these sterilized, lifeless, monetized, data mined, monitored addiction machines and let our creativity flourish again.</p> </blockquote> <p>The personal site isn't dead. It's just been forgotten in the commercialised, capitalist web of today. We owe it to ourselves to rediscover this lost art.</p> <p>We can still be creators for the sake of creating.</p> <p>We can still post content without someone else making money from it.</p> <p>So, once again my digital call to arms: build your own website. Make it fun. Make it pointless. But most importantly: make it <strong>yours</strong>.</p> <h2 id="appendix-look-at-these-cool-websites" tabindex="-1">Appendix: look at these cool websites</h2> <p>Here are some of the excellent personal and weird and wonderful websites that inspired me while writing this talk, and subsequently this post, and now, and forever.</p> <ul> <li><a href="https://henry.codes/">Henry from Online</a></li> <li><a href="https://lynnandtonic.com/">Lynn Fisher</a></li> <li><a href="https://makefrontendshitagain.party">Make Frontend Shit Again</a></li> <li><a href="https://carol.gg">Carol Gomez-Gilabert</a></li> <li><a href="https://ghost.computer/">Kara Brightwell</a></li> <li><a href="https://alistairshepherd.uk/">Alistair Shepherd</a></li> <li><a href="https://darn.es">David Darnes</a></li> <li><a href="https://sarajoy.dev">Sara Joy</a></li> <li><a href="https://www.nicchan.me/">Nic Chan</a></li> <li><a href="https://ohhelloana.blog">oh hello ana</a></li> <li><a href="https://cassie.codes">Cassie Codes</a></li> <li><a href="https://rknight.me">Robb Knight</a></li> <li><a href="https://chriskirknielsen.com">Christopher Kirk-Nielsen</a></li> <li><a href="https://machine-cat.space/home/">machine cat in space</a></li> </ul> The blog questions challenge 2025-02-18T00:00:00Z https://localghost.dev/blog/the-blog-questions-challenge/ <p>This takes me back to my teenage years, doing <a href="https://www.google.com/search?q=friday+five+2002+blogging">Friday Five</a> on my blog and later on the <a href="https://book-memes.livejournal.com/">sets of questions on LiveJournal that were known as memes</a>. Thanks <a href="https://sallylait.com">Sally</a> for the tag! &lt;3</p> <h2 id="why-did-you-start-blogging-in-the-first-place" tabindex="-1">Why did you start blogging in the first place?</h2> <p>As a teenager I blogged obsessively, posting about every minute detail of my life, every boy or girl I liked, every exam I was dreading. In researching my talk about personal websites (a blog post version of which is coming soon!) I found many of my old sites on the Wayback Machine, and had a great time archiving all the posts and simultaneously turning inside out with the cringe of it all.</p> <p>When I headed off to university I started going out and doing IRL things instead of being a little computer goblin, and that was it for blogging for about a decade.</p> <p>In 2018 I was preparing for my first proper conference talk and had gathered a load of great advice from more experienced speakers. Someone suggested I put it in a blog post, so I wrote one on Medium (I know, I know). The following year the <code>.dev</code> domain names became available and I snapped up <code>localghost.dev</code>, and decided I'd move that post from Medium to here. Over the coming years I wrote another post... and another post... and I was averaging about 4-5 posts a year until 2023 when I picked up the pace a bit.</p> <p>Sadly, that post was so full of now-broken Twitter embeds that I recently decided to archive it.</p> <h2 id="what-platform-are-you-using-to-manage-your-blog-and-why-did-you-choose-it-have-you-blogged-on-other-platforms-before" tabindex="-1">What platform are you using to manage your blog and why did you choose it? Have you blogged on other platforms before?</h2> <p>localghost.dev is an <a href="https://11ty.dev">Eleventy</a> shop, though in a previous incarnation it was built on <a href="https://gohugo.io/">Hugo</a>, which I liked well enough but not as much as Eleventy. My blog posts are markdown files, with different <a href="https://localghost.dev/blog/building-post-types-and-category-rss-feeds-in-eleventy/">categories</a> of post rendered differently.<br> I originally found Eleventy a little intimidating as I had no idea where to even start with it and the documentation wasn't super clear at the time, but <a href="https://piccalil.li">Andy</a> created a brilliant guide to getting started with Eleventy which made everything a whole lot more approachable. I haven't looked back, and it's still my platform of choice for new projects. I keep meaning to try Astro to see if it's as good as people say.</p> <p>In a past life, I used a whole host of different platforms: free web hosts with static HTML pages, Diaryland, Greymatter and Wordpress blogs hosted by random bloggers I'd met on the internet, Blogger, LiveJournal. I have very fond memories of Greymatter.</p> <h2 id="how-do-you-write-your-posts-for-example-in-a-local-editing-tool-or-in-a-panel-dashboard-that-s-part-of-your-blog" tabindex="-1">How do you write your posts? For example, in a local editing tool, or in a panel/dashboard that’s part of your blog?</h2> <p>Right now I'm writing this post in VS Code; if I'm writing longer-form posts that require a bit more editing and thinking, I'll use <a href="https://ia.net/writer">iA Writer</a> to get my thoughts down, then back to the IDE to get it ready to publish.</p> <p>I've briefly considered moving the writing to a CMS of some sort, mainly because I hate manually inserting images and faffing with image sizes, but since updating my site to use <a href="https://www.11ty.dev/docs/plugins/image/">Eleventy Image</a> that's become easier. I want to keep this a static site, plus I don't have the energy to self-host CMS software and keep it up to date, and considering how relatively infrequently I write anyway, it's not really worth it.</p> <h2 id="when-do-you-feel-most-inspired-to-write" tabindex="-1">When do you feel most inspired to write?</h2> <p>Not as often as I'd like. Occasionally I'll be struck by a great idea, write it down in Obsidian or something, and then forget I wrote it down. Now and then I'll think &quot;I could write a blog post today!&quot; and go spelunking for my list of ideas and pick one I feel like writing about. Sometimes those ideas get turned into conference talks instead, and sometimes my conference talks get turned into blog posts.</p> <p>Some weekends I'll sit down intending to bash out a few thoughts about something that's been on my mind, and then accidentally write a big post. Those are some of the best ones I think, because they tend to be more personal. I've found that sitting down and trying to force myself to write something is never a good idea. I've made peace with the fact that I post relatively irregularly, and I'm just happy when inspiration does strike. I'd love to be one of those folks who frequently posts useful technical tutorials and deep-dives on their blog, but I'm afraid you'll generally get the contents of my brain instead.</p> <p>I think that's powerful in itself, though: we're expected to be part of these big capitalist hellscape social media networks and post all our thoughts on there for them to comb for ad targeting parameters, and then try to sell us things. They can hide our posts from others if they don't like what we're writing about. Even on sites like Mastodon – less capitalist, still hellscape – the reply guys come out in full force and tell you exactly what they think of your post and how <em>actually</em> that's a bad take because blah blah blah... This is my own website, and I can post whatever I want.</p> <h2 id="do-you-publish-immediately-after-writing-or-do-you-let-it-simmer-a-bit-as-a-draft" tabindex="-1">Do you publish immediately after writing, or do you let it simmer a bit as a draft?</h2> <p>Generally I'll do it all at once, though for bigger ones (like the version of my personal websites talk I want to write) are so big that I need to do them in several sittings. I am about to leave the house, so there's a risk this one will be languishing on a branch for a while until I remember that I need to finish it.</p> <h2 id="what-s-your-favourite-post-on-your-blog" tabindex="-1">What’s your favourite post on your blog?</h2> <p>I have different favourites for the personal posts and the informative posts, so here's a few of each.</p> <p>Personal:</p> <ul> <li><a href="https://localghost.dev/blog/my-month-of-rest-and-relaxation/">My month of rest and relaxation</a></li> <li><a href="https://localghost.dev/blog/the-art-in-everyday-life">The art in everyday life</a></li> <li><a href="https://localghost.dev/blog/remembering-the-early-00s-teen-website-scene/">Remembering the early 00s teen website scene</a></li> </ul> <p>Useful:</p> <ul> <li><a href="https://localghost.dev/blog/the-right-tag-for-the-job-why-you-should-use-semantic-html/">The right tag for the job: why you should use semantic HTML</a></li> <li><a href="https://localghost.dev/blog/just-because-you-can-doesnt-mean-you-should-the-meter-element/">Just because you can doesn't mean you should: the &lt;meter&gt; element</a></li> </ul> <h2 id="any-future-plans-for-your-blog-maybe-a-redesign-a-move-to-another-platform-or-adding-a-new-feature" tabindex="-1">Any future plans for your blog? Maybe a redesign, a move to another platform, or adding a new feature?</h2> <p>I will almost certainly add a new theme at some point, because more is more. I'd also like to start collating some of my weird and wonderful pet <a href="https://localghost.dev/projects">projects</a> under the <code>localghost.dev</code> banner rather than on individually hosted sites.</p> <p>I'm also planning on starting a webring for technopessimists based off of a thread of very tired people on Mastodon, so watch this space.</p> <h2 id="who-s-next" tabindex="-1">Who’s next?</h2> <p>I'm relatively late to the party and not sure who hasn't done this yet, so if you're reading this and you haven't been tagged before, this is me tagging you!</p> Good links: 16 February 2025 2025-02-16T00:00:00Z https://localghost.dev/blog/good-links-2025-02-16/ <ul> <li><a href="https://keith.is/blog/you-are-not-meant-to-scale/">You Are Not Meant To Scale - Keith Kurson</a> - “Take a deep breath, and say out loud: I am not a machine, I am not meant to scale. You have a finite amount of energy, and a community of people around you who can use that energy. You can use that energy, to make it through the day, which is the most important thing. Waking up tomorrow is the name of the game.”</li> <li><a href="https://www.miriamsuzanne.com/2025/02/12/tech-ai-wtf/">Tech continues to be political - Miriam Suzanne</a> - “There are certainly a number of people raising alarms or expressing frustration, but we’re often dismissed as uninformed. Based on every conference I’ve attended over the last year, I can absolutely say we’re a fringe minority. And it’s wearing me out. I don’t know how to participate in a community that so eagerly brushes aside the active and intentional/foundational harms of a technology. In return for what? Faster copypasta? Automation tools being rebranded as an “agentic” web? Assurance that we won’t be left behind?”</li> <li><a href="https://css-irl.info/debating-the-merits-of-llms/">CSS { In Real Life } | Debating the Merits of LLMs</a> - “ An LLM [trained on thousands of documents], while useful, shouldn’t invent new information. It processes the text that already exists, not the science behind it, and if it appears to offer up something new then that should be met with the utmost scrutiny. And it remains to be seen whether they (and others like them) will be worth the extraordinary amount of energy and resources that AI demands.”</li> </ul> Good links: 9 February 2025 2025-02-09T00:00:00Z https://localghost.dev/blog/good-links-2025-02-09/ <ul> <li><a href="https://timsh.org/tracking-myself-down-through-in-app-ads/">Everyone knows your location</a> - It’s quite jarring to see the kind of information that gets sent to the highest bidder from seemingly innocuous apps.</li> </ul> My month of rest and relaxation 2025-01-16T00:00:00Z https://localghost.dev/blog/my-month-of-rest-and-relaxation/ <p>After 4 (cumulative) years of service at my job in late 2023, I became eligible for a 3-month paid sabbatical (honestly what a perk). My only prior experience of a work sabbatical had been the offer of a 6-month paid sabbatical after <em>25 years</em> of service at a past employer, so needless to say, this was very exciting indeed.</p> <p>Cautious of being alone with my thoughts for 3 months – my husband couldn’t take that much time off work – I decided to break it up into 3 month-long blocks and take them spread throughout the following couple of years. I took the first month in September 2024, a welcome rest after an intense few months on a particularly difficult project. As with any amount of free time of more than a couple of days, I felt like I was supposed be making the most of this time off and doing as much as humanly possible. I decided I’d learn new (non-work-related) skills; improve the house; do stuff in the garden. I did a bit of that. I started an electronics project to dismantle, clean and rewire a vintage Jubilee line door button to turn it into a Philips Hue lightswitch, except I got stuck on the part of the instructions where I had to actually wire it up to power the LEDs, and realised my knowledge of electrical engineering was so basic that I actually had to go back and learn that stuff first. I promptly forgot everything I learned as soon as I went back to work.</p> <p>I strategically booked the second part of my sabbatical over Christmas and vowed that I was going to do as little as humanly possible for the first two weeks, then have a productive two weeks. As a sabbatical project I thought I’d have a go at learning some reverse engineering, as I’d done some CTFs at work and really enjoyed them, but it turned out I enjoyed them because they’re puzzles and I’m an absolute puzzle fiend. Re-learning how assembly works is <em>not</em> what I wanted to be spending my time doing (I learnt a fair bit about different processor architectures and low-level languages during my masters, but lack of use – and a very boring lecturer – has meant it’s all a distant memory), so I quickly abandoned that plan.</p> <p>Instead, over the last three and a half weeks I’ve become such a potato that I’m considering changing my name to Maris Piper. I started (and sunk lots of hours into) replaying The Witcher 3 on PS5, played many many hours of La Mulana 2 in a horizontal position (and cracked the Steam Deck screen and <a href="https://localghost.dev/blog/i-repaired-my-steam-deck-and-it-was-fine-actually/">replaced it myself</a>), did a lot of leisurely cooking, planned the meals for our upcoming choir retreat. We did our usual quiet New Year tradition of having my friend over and playing board games, though this year we did manage to make it to midnight and walked up the hill in our local park to watch the fireworks over the river.</p> <p>My husband took some time off over Christmas as well, and we spent some great time together, visiting a very Christmassy National Trust house, going to the garden centre (haha just kidding that was hell on earth at that time of year), going out for dinner, playing video games together.</p> <p>I had the time to sit and arrange TWO songs for choir: <a href="https://www.youtube.com/watch?v=GR3Liudev18">Pink Pony Club by Chappell Roan</a> (the most recent song I’ve ever arranged, by a solid 20 years), and <a href="https://www.youtube.com/watch?v=dZLfasMPOU4">Stacy’s Mom by Fountains of Wayne</a>, which I’m very excited to teach in February. If you're interested, here are the arrangement recordings: <a href="https://drive.google.com/file/d/1LgXlMpsiaNr8LkFKs0APhM4tIwB0ca8W/view?usp=sharing">Pink Pony Club</a> and <a href="https://drive.google.com/file/d/1yVwfxDeCxTF3K4obxac8oUBzP1Msulo3/view?usp=drive_link">Stacy's Mom</a>.</p> <p>I picked up my knitting again after ignoring it for two years, and found it was a great way to keep my attention on the TV instead of looking at my phone. Yesterday I watched 7 episodes of Veronica Mars while knitting. (Can you believe I'd never watched it until now?)</p> <p>Boris the dog came to stay as well, which vastly increased the desire to sit on the sofa playing video games (he likes to fall asleep with his head on your lap).</p> <p>I went out a bit too:</p> <ul> <li>before Christmas I visited a friend who’s just had a baby, and then went to Oxford Street and regretted my life choices</li> <li>met up with a friend at the Photographers’ Gallery in Soho and then had a delicious cardamom bun at Söderberg</li> <li>got my hair cut at a tiny but well-rated local hairdressers and I came away with a nice bob with a 50/50 chance of looking like <a href="https://external-content.duckduckgo.com/iu/?u=http%3A%2F%2Fimages.complex.com%2Fcomplex%2Fimage%2Fupload%2Fc_limit%2Cw_680%2Ff_auto%2Cfl_lossy%2Cpg_1%2Cq_auto%2Flyizxjguhaksia3shnfb.jpg&amp;f=1&amp;nofb=1&amp;ipt=f0d9bac6826a5d43b6dcc791819f449c1a3ef576f00c89fb0c21b4656f2177ac&amp;ipo=images">Javier Bardem in No Country for Old Men</a> as soon as I try to style it</li> <li>went to see the <a href="https://designmuseum.org/exhibitions/barbie-the-exhibition">Barbie exhibition</a> at the Design Museum in Kensington, which was decent but probably not worth the trek across London (and honestly disappointed at the lack of <a href="https://en.wikipedia.org/wiki/Barbie_Fashion_Designer">Barbie Fashion Designer</a> in the collection).</li> </ul> <h3 id="my-month-of-re-evaluation" tabindex="-1">My month of re-evaluation</h3> <p>This time off also gave me opportunity for some reflection. I spent a lot of my late 20s defining myself by my career: I was a software developer first, and a person with hobbies and interests second. I was a Woman in Tech™, the only woman on my CS masters, and one of only a handful in the tech circles I moved in, so I had to Represent. I joined all the Women in Tech things. I went to tons of meetups (there were many more before the pandemic), I started going to and speaking at conferences. I loved my job and I didn’t understand why people would work jobs they didn’t love. I inexplicably made it onto some “Women in Tech Power List” in 2019. I was relatively well-known in web dev Twitter circles before it got bought out and I left in 2022; Twitter was a big part of my identity, which is horribly sad in retrospect. The most important thing was progression, being promoted and being the best (shout out to my fellow grammar school folks with lifelong overachievement complexes). I had lofty ambitions: I’d climb the ladder, and be a CTO someday!</p> <p>(Having subsequently seen many CTOs in action over the years, I am fairly convinced this is not a role I want for myself. Props to CTOs.)</p> <p>Having time to spend doing hobbies, hanging out with the dog, pottering around the house and <em>not thinking about work</em> has made me really redefine how I see myself. In recent times I had worried a lot that I was some kind of fraud turning up at these web conferences and chatting to the friends I’d made there, when my job hardly involves web development any more and I don’t even write much about tech these days because I don’t have the energy or will to do so.</p> <p>I’m not a software developer first and foremost, I’m a musician who arranges music for a choir; I’m a cook and a baker; I’m a fiend for puzzles to an embarrassing degree; I’m a fledgling gardener; I sink hours of my life into video games; I’m a person who sometimes does fun things on the web but mostly doesn’t.</p> <p>I still like writing code, and I like solving problems. I’m good at what I do. But when I put so much pressure on myself to be The Best At Everything And Also Promoted Quickly And Also Why Am I Not Enjoying This As Much As I Should Be I come away feeling like I’m bad at my job or doing something wrong.</p> <p>I am grateful that I am paid well, I really like the folks I work with, and most all of that my job allows me to switch off when I finish work and enjoy the things that bring me the most joy. Of course I still have ambition, but it's not all-consuming like it used to be. I don’t have work Slack on my phone, and I can count the times I’ve worked past 6 on... two fingers? I can be Sophie, the musician/gardener/baker/potato who goes to work and does a good job and comes home and cuddles the dog. That sounds like a much more appealing identity to me.</p> I repaired my Steam Deck and it was fine, actually 2025-01-13T00:00:00Z https://localghost.dev/blog/i-repaired-my-steam-deck-and-it-was-fine-actually/ <p>For Christmas 2022, I bought my husband a Steam Deck. It's a handheld games console that runs a version of Linux (so you can also use it as a computer and plug it into a monitor, if you want), and works seamlessly with nearly every game in your Steam Library. It's GREAT. I 100% use it more than he does, to play all manner of weird and wonderful things. It plays older games perfectly, and I even managed to play a bit of Baldur's Gate on there when it came out and my husband was playing it on the PS5 (and I just couldn't wait my turn). Had to crank the graphics down a bit and the battery didn't last super long, but it still ran!</p> <p>Over the past Xmas period I played an unholy amount of La Mulana 2. A few weeks back I noticed that what I thought was a smear on the screen was actually a crack. I have no idea how it happened – I think I must have dropped it or something? It was well out of its warranty by this point, so I looked into repair options and was delighted to see that Valve actually recommend that you try and repair it yourself. They sell official repair parts through iFixit, who also have brilliant step-by-step instructions with really clear pictures. I've built a (granted, much larger) PC before, and I fixed the spring in my PS5 controller trigger, so I figured I'd give it a go.</p> <p>So, I ordered a <a href="https://www.ifixit.com/en-gb/products/steam-deck-64gb-or-256gb-screen">replacement screen kit</a>, which comes with an iOpener kit that should be useful for the other small electronics I inevitably break somehow in the future. I also picked up a <a href="https://www.ifixit.com/en-gb/products/magnetic-project-mat">magnetic project mat</a>: it looks a bit like a cutting mat, but it's magnetic and comes with an erasable OHP pen.</p> <figure><picture><source type="image/webp" srcset="https://localghost.dev/img/WMdUg0Cmfi-280.webp 280w, https://localghost.dev/img/WMdUg0Cmfi-640.webp 640w, https://localghost.dev/img/WMdUg0Cmfi-960.webp 960w" sizes="auto"><img loading="lazy" decoding="async" src="https://localghost.dev/img/WMdUg0Cmfi-280.jpeg" alt="An ipad is propped up on the desk showing instructions for repairing the Steam Deck. In front is a white project mat with a grid on, and various tiny screws in the top 4 squares with what they are for written underneath: Back outer x4, Back inner x4, shield x3, SSD" width="960" height="720" srcset="https://localghost.dev/img/WMdUg0Cmfi-280.jpeg 280w, https://localghost.dev/img/WMdUg0Cmfi-640.jpeg 640w, https://localghost.dev/img/WMdUg0Cmfi-960.jpeg 960w" sizes="auto"></picture></figure> <p>The iFixit instructions were really clear with the exception of the one about <a href="https://www.ifixit.com/Guide/How+to+Apply+Thermal+Paste/744">applying thermal paste</a>, which said to apply thermal paste &quot;[using] the application method recommended for your specific processor type&quot;. While the Steam Deck has an AMD processor, it's a custom CPU as far as I can see, and the Arctic Silver website they linked didn't have it listed. But I decided to go for a good old-fashioned pea-sized blob of paste, and it seems to be fine.</p> <figure><picture><source type="image/webp" srcset="https://localghost.dev/img/u7PUKqgyVs-280.webp 280w, https://localghost.dev/img/u7PUKqgyVs-640.webp 640w, https://localghost.dev/img/u7PUKqgyVs-960.webp 960w" sizes="auto"><img loading="lazy" decoding="async" src="https://localghost.dev/img/u7PUKqgyVs-280.jpeg" alt="The open back of a Steam Deck: you can see the fan, the battery, and the motherboard along with a lot of ribbon cables and buttons." width="960" height="720" srcset="https://localghost.dev/img/u7PUKqgyVs-280.jpeg 280w, https://localghost.dev/img/u7PUKqgyVs-640.jpeg 640w, https://localghost.dev/img/u7PUKqgyVs-960.jpeg 960w" sizes="auto"></picture></figure> <p>Overall, it went really well! I managed to replace the screen, and the hardest thing was actually applying the fiddly little adhesive stickers. To get the screen off, I first had to heat the adhesive – I used the microwaveable gel-filled iOpener tool that came in the kit, but you can use a hairdryer as well.</p> <div class="content-grid"> <picture><source type="image/webp" srcset="https://localghost.dev/img/VBjy90R8yB-280.webp 280w, https://localghost.dev/img/VBjy90R8yB-640.webp 640w, https://localghost.dev/img/VBjy90R8yB-960.webp 960w" sizes="auto"><img loading="lazy" decoding="async" src="https://localghost.dev/img/VBjy90R8yB-280.jpeg" alt="A black tube labelled 'iOpener' laid on the top edge of the Steam Deck screen" width="960" height="720" srcset="https://localghost.dev/img/VBjy90R8yB-280.jpeg 280w, https://localghost.dev/img/VBjy90R8yB-640.jpeg 640w, https://localghost.dev/img/VBjy90R8yB-960.jpeg 960w" sizes="auto"></picture> <picture><source type="image/webp" srcset="https://localghost.dev/img/h8IbgYHXgo-280.webp 280w, https://localghost.dev/img/h8IbgYHXgo-640.webp 640w, https://localghost.dev/img/h8IbgYHXgo-960.webp 960w" sizes="auto"><img loading="lazy" decoding="async" src="https://localghost.dev/img/h8IbgYHXgo-280.jpeg" alt="The front of the Steam Deck with no screen on it - instead there is a metal recess where the screen would go" width="960" height="720" srcset="https://localghost.dev/img/h8IbgYHXgo-280.jpeg 280w, https://localghost.dev/img/h8IbgYHXgo-640.jpeg 640w, https://localghost.dev/img/h8IbgYHXgo-960.jpeg 960w" sizes="auto"></picture> </div> <picture><source type="image/webp" srcset="https://localghost.dev/img/-V5E3fMQeL-280.webp 280w, https://localghost.dev/img/-V5E3fMQeL-640.webp 640w, https://localghost.dev/img/-V5E3fMQeL-960.webp 960w" sizes="auto"><img loading="lazy" decoding="async" src="https://localghost.dev/img/-V5E3fMQeL-280.jpeg" alt="The Steam Deck with its new screen installed, switched on and showing the home page with the video games La Mulana, Gorogoa and Disney Dreamlight Valley" width="960" height="720" srcset="https://localghost.dev/img/-V5E3fMQeL-280.jpeg 280w, https://localghost.dev/img/-V5E3fMQeL-640.jpeg 640w, https://localghost.dev/img/-V5E3fMQeL-960.jpeg 960w" sizes="auto"></picture> <p>I did have one near miss, though, but not until <em>after</em> the repair: when I reassembled the deck I noticed the right trackpad wasn't working, and after a bit of googling and opening it up again it turned out that I'd not closed the latch on the ribbon cable connector properly. So I sorted that, and put the back case back on... only to realise I'd forgotten to take the Micro SD card out (the iFixit instructions tell you in <em>red lettering</em> right at the start to do that before you do anything else, and I did do that the first time round, but forgot I was supposed to do it when I opened it up after the repair). As I tried to put the back on, it snapped in half, leaving the half with the chip on stuck inside the SD card slot. I was slightly concerned I'd end up having to send it off for repair after all, but miraculously I managed to coax it out after some gentle prying with a pair of long-nosed tweezers, being careful not to damage anything. I tested it with another Micro SD card I had lying around, and thankfully it was fine. The only thing we lost there was whichever games were installed on the Micro SD card as opposed to the internal SSD, but we can just redownload those. It could easily have got permanently stuck or I could've damanged the contacts somehow. A lucky escape!</p> <p>Because the Steam Deck was designed to be repaired, the various parts are well-labelled: the cables for things like the buttons have each end labelled so it's obvious which end goes in the motherboard.</p> <figure><picture><source type="image/webp" srcset="https://localghost.dev/img/4lHiJQOcIL-280.webp 280w, https://localghost.dev/img/4lHiJQOcIL-640.webp 640w, https://localghost.dev/img/4lHiJQOcIL-960.webp 960w" sizes="auto"><img loading="lazy" decoding="async" src="https://localghost.dev/img/4lHiJQOcIL-280.jpeg" alt="An orange ribbon cable, held up to show both ends: one is marked &quot;MB&quot; and the other one is marked &quot;DB&quot;" width="960" height="720" srcset="https://localghost.dev/img/4lHiJQOcIL-280.jpeg 280w, https://localghost.dev/img/4lHiJQOcIL-640.jpeg 640w, https://localghost.dev/img/4lHiJQOcIL-960.jpeg 960w" sizes="auto"></picture></figure> <p>I'm a big proponent of the <a href="https://www.europarl.europa.eu/news/en/press-room/20240419IPR20590/right-to-repair-making-repair-easier-and-more-appealing-to-consumers">right to repair</a>, and it makes me happy to see things like this, and official replacement parts easily available! It's absolutely WILD that so many companies don't want us to repair their products, and would rather either make us pay for a replacement or encourage us to buy a brand new product. There is just <em>so much waste</em> and so much unnecessary expenditure.</p> <p>I mentioned the PS5 controller before: the left trigger suddenly had no friction whatsoever and kept being activated with the slightest nudge. It turns out the tiny spring that keeps it under tension had snapped. I got a replacement for a few quid off eBay, and followed a tutorial on YouTube, saving me the cost of repair.</p> <p>If you've never repaired your own electronics, I really encourage you to give it a go. Youtube and websites like iFixit are full of guides and step-by-step tutorials. iFixit also sells a lot of replacement parts, both genuine (in the case of Valve products) and aftermarket. The worst thing that happened in this whole process was because I have a terrible memory and wasn't diligent enough to follow the instructions thoroughly a second time, leaving the Micro SD card in. Following the actual steps was a breeze, and electronics are really not that scary, I promise!</p> 2024: The year in lists 2024-12-26T00:00:00Z https://localghost.dev/blog/2024-the-year-in-lists/ <p>It’s Boxing Day and I’m a small pile on the sofa. We successfully Did Christmas at ours this year, and I never want to see another mince pie (until next year).</p> <p>So, what better time than now to look back on the year?</p> <p>Skip to bits you care about:</p> <ul> <li><a href="https://localghost.dev/blog/2024-the-year-in-lists/#the-year-in">The year in...</a> <ul> <li><a href="https://localghost.dev/blog/2024-the-year-in-lists/#big-life-things">...big life things</a></li> <li><a href="https://localghost.dev/blog/2024-the-year-in-lists/#conferences">...conferences</a></li> <li><a href="https://localghost.dev/blog/2024-the-year-in-lists/#gardening">...gardening</a></li> <li><a href="https://localghost.dev/blog/2024-the-year-in-lists/#travel">...travel</a></li> <li><a href="https://localghost.dev/blog/2024-the-year-in-lists/#books">...books</a></li> <li><a href="https://localghost.dev/blog/2024-the-year-in-lists/#podcasts">...podcasts</a></li> <li><a href="https://localghost.dev/blog/2024-the-year-in-lists/#music">...music</a></li> <li><a href="https://localghost.dev/blog/2024-the-year-in-lists/#video-games">...video games</a></li> <li><a href="https://localghost.dev/blog/2024-the-year-in-lists/#blog-posts">...blog posts</a></li> <li><a href="https://localghost.dev/blog/2024-the-year-in-lists/#christmas-dinner">...Christmas dinner</a></li> </ul> </li> </ul> <h2 id="the-year-in" tabindex="-1">The year in...</h2> <h3 id="big-life-things" tabindex="-1">...big life things</h3> <ul> <li>We bought a house! It's great! I have written at length about the house, but it's still so surreal to be living here. I now have my own office, so I'm working from home a lot more. We're hopefully getting a dog soon, as well. We hosted Christmas for the first time ever, and it was only moderately stressful.</li> <li>I've been driving a bit more, trying to get my confidence back up. I had a minor car accident in 2018 that was quite traumatic (nobody was hurt, thankfully) and haven't driven much since. Managed to drive myself to IKEA, and later on the dual carriageway to the out-of-town shopping centre. I still can't bring myself to drive at night (when the accident happened) but I'm hoping to knock that on the head soon. The goal is to be able to drive to my parents' down south without needing James to drive me.</li> <li>As I've got more senior in my role at work, my responsibilities have evolved and I found myself really struggling in a way I haven't before. Up until now, I've done fine context-switching constantly, and working in a very interrupt-driven way. I've found that as my work is less well-defined and the problems much more ambiguous – and <em>I'm</em> the one who has to decide what we do – I've been feeling completely overwhelmed and bad at my job. I thought I had too much on my plate, but when I listed everything out it wasn't actually that much. I'd always start to-do lists with the best intentions to try and keep on top of things, but would constantly forget I had them. After talking to various people and reading various things, I actually started to wonder if I had mild ADHD, and it was finally becoming a problem. I've asked to be referred for an assessent via NHS <a href="https://adhduk.co.uk/right-to-choose/">Right to Choose</a> as the standard waiting list for ADHD assessment can take <em>years</em>.</li> </ul> <h3 id="conferences" tabindex="-1">...conferences</h3> <p>I stuck to my 4-conferences-a-year rule from last year, and it was great. Next year I might even do LESS. I love speaking at conferences, and travelling, but I don’t think I have the inspiration or energy to come up with another talk any time soon – so I’ll see who else wants to hear my migrations talk, but I may end up just doing a couple of speaking gigs next year.</p> <ul> <li>In <strong>May</strong> I finally got the chance to speak at one of my fave UK conferences, Leeds-based <a href="https://heypresents.com">All Day Hey</a>. It was the debut of the talk I wrote based on our experiences migrating to Typescript at Monzo: less of a technical talk than you might think, and more of “how to do any kind of migration, and maybe don’t actually do it, have you considered that”.</li> <li>I attended StaffPlus again in <strong>June</strong>. It continues to be the most relevant conference for what I actually do, but it's a thoroughly overwhelming experience with so many people as the event is combined with the massive Lead Dev. Apparently it's moving out of the Barbican next year, which hopefully means more tickets will become available for StaffPlus. It pains me how expensive it is; I had to borrow someone else's learning budget on top of mine to be able to buy the ticket.</li> <li>Popped along to Bristol in <strong>June</strong> for <a href="https://pixelpioneers.co">Pixel Pioneers</a>, a lovely web and UI/UX conference in Bristol. I keep meaning to go back to Bristol and explore some more, as it’s only a couple of hours on the train from London.</li> <li>In <strong>September</strong> we travelled to Freiburg where I was lucky enough to be part of the roster for Smashing Conference. I met some amazing folks, and the organisers are some of the loveliest people I’ve ever met. I tried in vain to practise my extremely rusty German.</li> <li><strong>November</strong> brought with it <a href="https://beyondtellerrand.com">Beyond Tellerrand</a> Berlin. It was lovely to visit Berlin again, although I didn’t get much chance to explore. It was great to hang out with some of the folks I’d met at other conferences, and meet some new friends too! Alas, this coincided with FFConf, so I didn’t get to attend that this year.</li> </ul> <p>Next year I’ve got <a href="https://jsheroes.io/">JSHeroes</a> lined up so far, plus one more that’s yet to be announced. I’ve heard so many nice things about JSHeroes, so I’m really excited to be speaking there!</p> <h3 id="gardening" tabindex="-1">...gardening</h3> <p>2024 was the year I finally got a garden. Well... two gardens, actually! We have a front and a back garden. When we moved in, the front garden was horribly overgrown, and the back garden was just long grass.</p> <ul> <li>We dug in two beds in the back garden, and moved the path. I've now got a potting shed at the back, which is my own little gardening den. Next step is to get a little greenhouse.</li> <li>James built me a couple of raised beds, including a little one for my herb garden. Whenever I need sage, parsley, thyme, chives, rosemary or oregano, I can just pop outside. (I've got basil growing merrily in a pot on the kitchen windowsill, as well.)</li> <li>There was a sad-looking apple tree in a pot in our back garden when we moved in, and it's now been planted in the front. Hoping that will grow a bit more!</li> <li>My acer, which for years was wind-battered and dehydrated by our front door, is now in the ground in a more sheltered spot. We've got heavy clay, but there are plenty of acers around in our neighbours' gardens, so I'm hopeful this one will do just as well.</li> <li>I have killed several plants already, including a brand new clematis.</li> <li>We hacked down the tough, overgrown shrubs in the front, allowing the hypericums (aka St John's Wort) and weigela to grow back a lot softer and nicer. We'd like to plant a handful more evergreen shrubs along the driveway to stop litter from being blown into the garden.</li> <li>I spent a lot of time fighting dandelions and brambles, to varying degrees of success. I ended up giving up on the entirely-organic approach for the brambles, and poisoning the shit out of them, but it did at least seem to work.</li> <li>I've planted a load of garlic in the herb bed (for lack of more appropriate space), as well as a ton of alliums, tulips, daffs and various spring bulbs in pots.</li> <li>Hoping that the layer of mulch I put down on one of the beds will break down the clay a bit for the spring.</li> <li>The raised bed on the front patio now has a seasonal container for some pizazz, plus a handful of heucheras and other bedding plants around it. Stuck some bulbs in there as well, and I am now at war with a fox which keeps trying to dig them up.</li> </ul> <h3 id="travel" tabindex="-1">...travel</h3> <p>I didn’t do any Big Holidays this year, because of the house. Instead, we did a few little trips - James came with me to Freiburg, and we visited the New Forest as well.</p> <p>This year’s lads’ trip brought us to Rye, in East Sussex. My second time in the area, but lovely to come back. We stayed in a lovely townhouse, went birdwatching at RSPB Dungeness, and walking on the marshes. <a href="https://www.dungeness-nnr.co.uk/">Dungeness</a> is an endlessly curious place and a site of special scientific interest (SSSI), with a unique ecosystem (the largest expanse of shingle in the UK, and the UK’s only desert) and two decommissioned nuclear power plants that loom quietly over the reserve. The reserve itself is peppered with tiny cabins, some built from old rail carriages, and one belonging to the late artist Derek Jarman. We popped into a few that had been converted into studios and galleries.</p> <figure><picture><source type="image/webp" srcset="https://localghost.dev/img/Yf8CAQ-Ek8-280.webp 280w, https://localghost.dev/img/Yf8CAQ-Ek8-640.webp 640w" sizes="auto"><img loading="lazy" decoding="async" src="https://localghost.dev/img/Yf8CAQ-Ek8-280.jpeg" alt="Dungeness, an expanse of shingle with patches of scrub, grass and sea kale. There's a boardwalk in the distance with a couple walking on it. It's sunny, and the sky is a deep blue." width="640" height="480" srcset="https://localghost.dev/img/Yf8CAQ-Ek8-280.jpeg 280w, https://localghost.dev/img/Yf8CAQ-Ek8-640.jpeg 640w" sizes="auto"></picture></figure> <h3 id="books" tabindex="-1">...books</h3> <p>2024 was the year I caved and started reading romantasy books. Unlike the time some years ago that I read Fifty Shades of Grey and Twilight because I felt like I should at least read them before mocking them – and they deserved every single insult – I read ACOTAR and then immediately hoovered up the sequels. They are very enjoyable and so easy to read, which is honestly exactly what I needed this year.</p> <p>I feel like I’ve done a less good job than last year of reading the kind of books you tell people about and don’t immediately follow with some kind of excuse, partly because I’ve been perpetually exhausted and the only thing I manage to get through successfully is the literary equivalent of a Big Mac. But sometimes you just need that junk food fix in the name of escapism, and you can eat your Booker-prize-winning vegetables when you have a bit more energy.</p> <p>Or maybe I should just stop being ashamed for liking this kind of book, that works too.</p> <p>Side note: I feel like I need a separate list for “most tenuous analogies of the year”.</p> <p>Let's get the romantasy ones out of the way first:</p> <p>Yes, I read all of the Sarah J. Maas books: they can all pretty much be summed up as &quot;Headstrong female heroine meets her fated true love and saves the world from the Big Bad over the course of 3-7 books&quot;. I will obviously read more of them.</p> <ul> <li><a href="https://uk.bookshop.org/p/books/throne-of-glass-sarah-j-maas/7136220?ean=9781526635297">Throne of Glass series</a>: this series is probably the best out of all Maas' work, but the first couple of books are not quite as good as the rest. I think the latter books are genuinely good.</li> <li><a href="https://uk.bookshop.org/p/books/a-court-of-thorns-and-roses-sarah-j-maas/654528?ean=9781526605399">A Court of Thorns and Roses series</a>: I enjoyed these a lot, ridiculous as they are. I think everyone makes some questionable choices, but it's still a lot of fun.</li> <li><a href="https://uk.bookshop.org/p/books/house-of-earth-and-blood-sarah-j-maas/3121372?ean=9781526663559">Crescent City series</a>: a fantasy romance with a modern-day setting. First book was fun, second was all right, third read like bad fanfiction.</li> <li><a href="https://uk.bookshop.org/p/books/paladin-s-grace-t-kingfisher/7672899?ean=9780356524313">Paladin’s Grace by T. Kingfisher</a>: fantasy romance, except the characters were over the age of about 25, and the male protagonist was a paladin who likes to knit socks for people. Delightful.</li> <li><a href="https://www.waterstones.com/book/from-blood-and-ash/jennifer-l-armentrout/9781952457760">From Blood and Ash series by Jennifer L. Armentrout</a>: see description for all the Sarah J. Maas books. Literally exactly the same.</li> <li><a href="https://uk.bookshop.org/p/books/fourth-wing-your-new-fantasy-romance-obsession-starts-here-rebecca-yarros/7400234?ean=9780349436999">The Fourth Wing</a>: I kind of wish I could un-read this, because the writing is garbage and it's honestly so obvious the &quot;bad guy&quot; is going to be the love interest because of how extremely thirsty the protagonist is from the first page. There's also dragons, and the main characters getting horny because the dragons are getting it on. You see why I want to erase this from my memory. I don't really understand why people like this so much.</li> </ul> <p>The rest of this year’s reads, in no particular order:</p> <ul> <li><a href="https://en.wikipedia.org/wiki/Children_of_Time_(novel)">Children of Time by Adrian Tchaikovsky</a>: a sci-fi set in the far future where we've gone and trashed earth so they're trying to populate a new planet with primates and some virus to fast-forward evolution, only it doesn't quite go to plan.</li> <li><a href="https://uk.bookshop.org/p/books/a-darker-shade-of-magic-collector-s-edition-v-e-schwab/1350424?aid=1200&amp;ean=9781785657740">A Darker Shade of Magic by V.E. Schwab</a>: a fantasy story about a magician who travels between parallel Londons.</li> <li><a href="https://uk.bookshop.org/p/books/sistersong-lucy-holland/1946693?ean=9781529039030">Sistersong by Lucy Holland</a>: a retelling of a traditional murder ballad <a href="https://en.wikipedia.org/wiki/The_Two_Sisters_(folk_song)"><em>The Twa Sisters</em></a>.</li> <li><a href="https://uk.bookshop.org/p/books/the-immortal-life-of-henrietta-lacks-rebecca-skloot/523054?ean=9781035038619">The Immortal Life of Henrietta Lacks by Rebecca Skloot</a>: a biography of Henrietta Lacks, the Black woman whose cancerous cells (known as HeLa cells) were taken and used for medical research without her knowledge. HeLa cells have led to significant developments in medical science, but she and her family never saw a penny. A really fascinating and heartbreaking read.</li> <li><a href="https://en.wikipedia.org/wiki/The_Three-Body_Problem_(novel)">The Three-Body Problem by Cixin Liu</a>: didn't quite finish this one, it just went on a bit.</li> <li><a href="https://uk.bookshop.org/p/books/the-essex-serpent-now-a-major-apple-tv-series-starring-claire-danes-and-tom-hiddleston-sarah-perry/2251326">The Essex Serpent by Sarah Perry</a>: we actually watched the (very good) dramatisation of this on Apple TV with Claire Danes and Tom Hiddleston, and that spurred me to read the book. A Victorian paleontologist travels to a remote Essex village to investigate sightings of a so-called &quot;serpent&quot;</li> <li><a href="https://uk.bookshop.org/p/books/say-nothing-a-true-story-of-murder-and-memory-in-northern-ireland-patrick-radden-keefe/681700?ean=9780008159269">Say Nothing by Patrick Redden Keefe</a>: accounts of the Troubles in Northern Ireland, and the lives of some of the key figures in the IRA. It was a fascinating read.</li> </ul> <p>I read three books by Rebecca Kuang, and it wasn’t until I was halfway through Yellowface that I realised she was also R.F. Kuang who wrote the excellent <em>Babel</em>. She's now one of my favourite authors.</p> <ul> <li><a href="https://uk.bookshop.org/p/books/yellowface-rebecca-f-kuang/6923283?ean=9780008532819">Yellowface by Rebecca Kuang</a>: the story of a white author who takes credit for her late Chinese-American friend's work, and how it all unravels</li> <li><a href="https://uk.bookshop.org/p/books/babel-or-the-necessity-of-violence-an-arcane-history-of-the-oxford-translators-revolution-r-f-kuang/6627642">Babel by R.F. Kuang</a>: British imperialism, magical realism, linguistics and silver bars.</li> <li>Currently reading <a href="https://uk.bookshop.org/p/books/the-poppy-war-r-f-kuang/4139876?ean=9780008239848">The Poppy War by R.F. Kuang</a>: a girl from a poor province of a fictional empire based on China gets into a prestigious military academy and discovers a talent for shamanism.</li> </ul> <p>If you happen to be in central London any time soon (you poor soul), the big Waterstones on Piccadilly has a Booker library on the lower ground floor, with all the Booker prize winners and nominees over the years. It's worth a visit for some reading list inspiration!</p> <h3 id="podcasts" tabindex="-1">...podcasts</h3> <p>My most-listened podcasts this year were:</p> <ul> <li><a href="https://www.ifbookspod.com/">If Books Could Kill</a> – sardonic takedowns of well-known non-fiction books and political goings-on</li> <li><a href="https://switchedonpop.com/">Switched On Pop</a> – pop music, analysed and dissected</li> <li><a href="https://darknetdiaries.com/">Darknet Diaries</a> – stories from the world of cybersecurity</li> <li><a href="https://www.20k.org/">Twenty Thousand Hertz</a> – all things audio and sound.</li> <li>I haven’t listened to <a href="https://www.offmenupodcast.co.uk/">Off Menu</a> quite so much this year, but the recent Derren Brown episode made me laugh out loud in the street, which was moderately embarrassing.</li> <li>New to me this year was <a href="https://pca.st/podcast/56c0a1c0-79be-013c-dc74-0e76ec147af9">Close Your Eyes</a>: a drama thriller about a man who joins a cult to try and find his missing brother. It just gets weirder and weirder.</li> </ul> <h3 id="music" tabindex="-1">...music</h3> <p>I’ve officially had enough Taylor’s Versions, and I didn’t get tickets for the Eras Tour (unlike apparently everyone else??). Thought The Tortured Poet’s Department was boring and didn’t make it all the way through. Sorry Swifties.</p> <ul> <li><a href="https://music.apple.com/gb/album/brat-and-its-the-same-but-theres-three-more-songs-so-its-not/1750468113">BRAT - Charli XCX</a> (or technically <em>‌Brat and it’s the same but there’s three more songs so it’s not</em>): I definitely wasn’t immune to this absolutely ridiculous album’s charms, and some of the covers made the songs even better (<em>girl, so confusing</em> with Lorde is definitely the better version).</li> <li><a href="https://music.apple.com/gb/album/everyones-getting-involved-a-tribute-to-talking/1739474282">Everyone’s Getting Involved: A Tribute To Talking Heads’ Stop Making Sense - various artists</a>: millennials’ obsession with Talking Heads has led to this excellent covers compilation of songs from the iconic Stop Making Sense live album/film. I particularly love Paramore’s cover of <em>Burning Down The House</em>, and girl in red’s <em>Girlfriend is Better</em>.</li> <li><a href="https://music.apple.com/gb/album/no-obligation/1752949407">No Obligation - The Linda Lindas</a>: the second album by a young band making very good rock music with a sprinkling of riot grrrl inspiration.</li> <li><a href="https://music.apple.com/gb/album/hopes-and-fears-20/1729802956">Hopes and Fears 20 - Keane</a>: I was <em>thoroughly</em> obsessed with Keane in my teens. Posters, t-shirts, albums, posting on the official forums, curating a collection of bootleg mp3s. They somewhat undeservedly had a reputation as a very bland, middle-of-the-road band (probably because they were posh and got played on Radio 2), but I still think they’re absolutely brilliant. This will always be one of my favourite albums – it holds up so well – and I love a good remaster. We went to see them at the O2 for the 20th anniversary tour, and they’re as enthralling as they’ve ever been. Incredible songwriting, Tom Chaplin’s extraordinary voice, and beautiful instrumentation. This version also has some of my favourite B-sides on as well: <em>Walnut Tree</em>, <em>Snowed Under</em>, <em>To The End Of The Earth</em>.</li> <li><a href="https://music.apple.com/gb/album/copy-paste-vol-1-abridged-ep/1778506013">copy/paste vol.1 (abridged) - Garbage</a>: sneaking in at the last minute, this one only came out recently. I looove Garbage and these are some great covers – especially love the cover of Cities in Dust.</li> <li><a href="https://music.apple.com/gb/album/from-zero/1766137049">From Zero - Linkin Park</a>: I was never much of a Linkin Park fan (though I feel like I should’ve been), but I honestly can’t get enough of <em>The Emptiness Machine</em> and I think their new vocalist suits them so well. I’ll probably go back and listen to all the old stuff.</li> <li><a href="https://music.apple.com/gb/album/your-own-adventure/1691293640">Your Own Adventure - Hollow Hand</a>: I saw Caitlin Rose at Moth Club this year, and Hollow Hand were supporting (and also being Caitlin's backing band). This is a great country/folk rock album by a band hailing from Brighton.</li> <li><a href="https://music.apple.com/gb/album/lost-girls/1465892375">Bat for Lashes</a>: saw her play at the Barbican this year with my friend who's a big fan, and had only known about one of her songs, but I had such a great time. She really is excellent live. I think my favourite album is Lost Girls (linked) which is very new wave/80s-inspired.</li> <li>The <a href="https://music.apple.com/gb/album/the-rise-and-fall-of-a-midwest-princess/1698723205">Chappell Roan</a> hype train (I am on it): I have finally learned how to pronounce her name (as in <em>Sistine</em>, not <em>David La-</em>), and though I was late to the Chappell Roan party I absolutely love her music. It’s so well-produced, and so much fun.</li> <li>Speaking of well-produced, I’ve been arranging <em>Stacy’s Mom</em> by Fountains of Wayne for my choir, and it’s honestly SUCH a good song. We already knew Adam Schlesinger was a genius (he also co-wrote many of the songs for Crazy Ex-Girlfriend) but that song is just really, really excellent considering it was treated as just a joke song when it came out.</li> </ul> <h3 id="video-games" tabindex="-1">...video games</h3> <p>I spent many many hours playing video games, and we had some crackers this year. I’ve posted about many of the games I played in my “things I’ve been enjoying recently” posts, so this will be a lot of repeating myself, but think of it as... me reinforcing how good they are.</p> <ul> <li><a href="https://la-mulana.com/en/">La Mulana</a> and <a href="https://la-mulana.com/en/l2/">La Mulana 2</a>: a fiendishly difficult <a href="https://en.wikipedia.org/wiki/Metroidvania">Metroidvania</a> puzzle platformer set in ancient ruins, where you play an archaeologist searching for the secret of La Mulana (or his daughter searching for her father in the sequel). I've never persevered with a game this much, ever, and managed to make it about 95% of the way through – beating some really hard bosses! Thanking the many people who have published hints for these games on the internet.</li> <li><a href="https://www.annapurnainteractive.com/en/games/lorelei-and-the-laser-eyes">Lorelei and the Laser Eyes</a>: another puzzle exploration game but this time with escape-room vibes. I played this one with my husband, him driving and me helping to solve the puzzles because I am a puzzle fiend. This one is extremely odd, but very satisfying.</li> <li><a href="https://discoelysium.iam8bit.com/en-gb">Disco Elysium</a>: a story-based exploration game, where your character (a detective investigating a murder) evolves based on the choices you make. Very funny, and the world-building is incredible.</li> <li><a href="https://www.inscryption.com/gate">Inscryption</a>: do yourself a favour and don’t look up what this game is about before you play it. Creepy deck-builder with escape room elements.</li> <li><a href="https://www.playstation.com/en-gb/games/astro-bot/">Astro Bot</a>: an absolutely brilliant adventure platformer that very deservedly won Game of the Year. It’s a PS5 exclusive, and takes full advantage of the features of the console to an amazing extent – they really have done a fantastic job of the haptics.</li> <li><a href="https://www.inklestudios.com/heavensvault/">Heaven’s Vault</a>: described as an &quot;archaeological science-fiction adventure game&quot; (all exciting words), you play Aliya, an archaeologist on the hunt for a missing roboticist in a strange futuristic nebula in space. You slowly build up a vocabulary of the language of the Ancients, and begin to understand more about what came before.</li> <li><a href="https://www.riseofthegoldenidol.com/">Rise of the Golden Idol</a>: the sequel to The Case of the Golden Idol, this one is available for free if you’re a Netflix subscriber. A puzzling detective saga where you have to piece together what happened from clues in various scenes.</li> </ul> <h3 id="blog-posts" tabindex="-1">...blog posts</h3> <p>This year I published 16 blog posts, which is... considerably more than I expected given my mental state, if I'm honest. I didn't really write many technical ones, which I suppose checks out. I also set up automated posting from bookmarked links in Raindrop, and did it for a few months before my reading list overwhelmed me again.</p> <p>Some of my favourite posts:</p> <ul> <li><a href="https://localghost.dev/blog/the-art-in-everyday-life/">The art in everyday life</a></li> <li><a href="https://localghost.dev/blog/you-should-go-to-conferences/">You should go to conferences</a></li> <li><a href="https://localghost.dev/blog/just-because-you-can-doesn-t-mean-you-should-the-meter-element/">Just because you can doesn't mean you should: the &lt;meter&gt; element</a></li> <li><a href="https://localghost.dev/blog/2024-the-year-in-lists/blog/so-you-ve-decided-to-get-into-mechanical-keyboards/">So you’ve decided to get into mechanical keyboards</a></li> </ul> <h3 id="christmas-dinner" tabindex="-1">...Christmas dinner</h3> <p>Though I'm usually horribly pretentious when it comes to cooking – favouring recipes that require me to buy a jar of something by Belazu that's only available in a small number of Waitroses, that I use a tablespoon of and then it goes off in the back of my fridge – I went for the full trad Christmas Spread this year (with some outside contributions). Here's what I made (with any recipes), and my ratings out of 12 (days of Christmas). I leant heavily on Delicious Magazine's recipes, and some of them will stay, while others I won't bother making again.</p> <ul> <li><a href="https://www.kellybronze.co.uk/">KellyBronze turkey</a>: 7 swans-a-swimming/12. As it was the first time doing Christmas, I thought I'd splash out on a really great turkey. It was indeed a really excellent turkey – they're free-range, allowed to grow and run around in the woodlands etc, so have a really good flavour, and the cooking instructions were very simple and precise – but ultimately let down by the fact that it is, well, a turkey, and therefore the most mediocre of all poultry. I think next year I'll do something else. (I did, however, appreciate their guidance of &quot;Don't panic, it's just a big chicken&quot;.)</li> <li>Combination-of-everyone's-recipes potatoes with goose fat: 5 gold rings/12. I let the team down and I let myself down by making too many potatoes and not putting them in a single layer, so they didn't get crispy.</li> <li>Delicious Magazine's <a href="https://www.deliciousmagazine.co.uk/recipes/toasted-sourdough-bread-sauce/">bread</a> and <a href="https://www.deliciousmagazine.co.uk/recipes/ultimate-homemade-cranberry-sauce/">cranberry</a> sauces: 12 drummers drumming/12. The most heavenly, aromatic bread sauce I've ever had (even for a bread sauce denier such as myself), and gorgeous orangey cranberry sauce. Please halve the quantities for your own sanity (and freezer space). You can make these ahead, too. These two are going into my Christmas recipe hall of fame.</li> <li><a href="https://www.deliciousmagazine.co.uk/recipes/vichy-carrots/">Delicious Magazine's vichy carrots recipe</a>: 7 swans-a-swimming/12. I was already quite stressed by the time it came to dish up, and the water was supposed to evaporate and leave the carrots in a buttery glaze, but they were very much still underwater. The flavour was very good, but I would not repeat the experience.</li> <li><a href="https://www.deliciousmagazine.co.uk/recipes/sausage-sage-prune-and-onion-stuffing/">Delicious Magazine's sausage, sage, prune and onion stuffing recipe</a>: 11 pipers piping/12. Very good, but missing something. I think my favourite ever stuffing is Ottolenghi's <a href="https://jonoandjules.com/tag/arnolds-roast-chicken-with-caraway-and-cranberry-stuffing/">cranberry and caraway stuffing</a>, but that would probably be a bit much for Christmas. I made double of the Christmas stuffing recipe, and turned the other half into sausage rolls with some leftover puff pastry I had in the fridge.</li> <li><a href="https://www.deliciousmagazine.co.uk/recipes/fondant-sprouts-with-bacon-and-chestnuts/">Delicious Magazine's fondant sprouts with chestnuts and bacon</a>: 10 lords-a-leaping/12. A timeless combination, and charring the cut sides of the sprouts first gives it a lovely nutty flavour. I think I could probably have just roasted them, though.</li> <li><a href="https://www.deliciousmagazine.co.uk/recipes/butter-roast-parsnips-with-hazelnuts-and-sage/">Delicious Magazine's butter roast parnips with hazelnuts and sage</a>: 9 ladies dancing/12. Points deducted due to absence of blanched hazelnuts at local Co-op (and I forgot to put them on the Sainsbury's order). Anything in butter is good, and these were very nice indeed, but again: nobody ate the crispy sage leaves.</li> <li><a href="https://www.deliciousmagazine.co.uk/recipes/marmalade-pigs-in-blankets/">Delicious magazine's marmalade pigs in blankets</a>: 11 pipers piping/12 (can't really go wrong can you). The touch of marmalade is a lovely finish, though one point deducted because I now have nearly an entire jar of marmalade to use up.</li> <li><a href="https://www.deliaonline.com/recipes/occasions/christmas/christmas-100-recipes-to-freeze/traditional-braised-red-cabbage-with-apples">Delia's red cabbage</a>: 12 drummers drumming/12. This is the one recipe we make every Christmas in my family, and it's great every time (this year, Mum brought it). It's tart, aromatic, and comforting.</li> </ul> <h2 id="that-s-it-for-now" tabindex="-1">That’s it for now <!-- omit in toc --></h2> <p>Time to revert back into liquid form.</p> <p>Best wishes for the new year, and I hope it’s everything you want it to be.</p> Good links: 8 December 2024 2024-12-08T00:00:00Z https://localghost.dev/blog/good-links-2024-12-08/ <ul> <li><a href="https://phirephoenix.com/blog/2024-11-05/modernity">modernity is stupid: a rant not about politics</a> - every word of this post resonates, and you should read it.</li> </ul> Some things I've been enjoying recently (November 2024 edition) 2024-11-30T00:00:00Z https://localghost.dev/blog/some-things-i-ve-been-enjoying-recently-november-2024-edition/ <p>A roundup of fun things and projects.</p> <h2 id="tv" tabindex="-1">TV</h2> <p>I mentioned to my husband that I'd never watched Life on Mars, so we're watching it all the way through, and it's brilliant, obviously. I'm horribly annoying when watching things that have been filmed in places I know because I will a) announce where it is and b) complain if a character goes from one place to another in a way that isn't geographically possible. For example, the most recent series of You involved the main character walking home from his place of work – the campus of Royal Holloway – through Spitalfields market, which is a <a href="https://www.google.com/maps/dir/royal+holloway/Spitalfields+Market,+Brushfield+Street,+London/@51.464751,-0.4868559,11z/data=!3m2!4b1!5s0x48761cb3f857c8a5:0x30e5c1c4eb5d18f5!4m14!4m13!1m5!1m1!1s0x48767755f3ce74c9:0xd04339084bd84568!2m2!1d-0.5667494!2d51.4247097!1m5!1m1!1s0x48761cb4130ec9fd:0x77c1110edca01be0!2m2!1d-0.0760688!2d51.5197441!3e2?entry=ttu&amp;g_ep=EgoyMDI0MTExOS4yIKXMDSoASAFQAw%3D%3D">9 hour walk</a>. Anyway, Life on Mars is set in Manchester in the 1970s, so there are about 4 streets in the Northern Quarter/Ancoats area that pop up repeatedly (Dean Street <em>always</em> shows up in films and TV when they want 'early-to-mid century-looking street'), as well as a courthouse I'm fairly sure was the university humanities building where I had a lot of lectures. James is a patient man.</p> <p><em>He'd</em> never watched Look Around You (which my university housemates and I were thoroughly obsessed with) so we've been re-watching that as well and I'm happy he's enjoying it. For the uninitiated, it's a parody science programme – the first series parodies 1980s educational programmes and the second series is much like Tomorrow's World, with presenters (including Olivia Colman) and bizarre scientific inventions. Featuring cameos from such icons as Simon Pegg, Edgar Wright, Nick Frost, Mark Heap and Harry Enfield. Series 1 is probably better, but nothing tops the actor Kevin Eldon singing <a href="https://www.youtube.com/watch?v=vwyuB8QKzBI">Machadaynu</a>.</p> <p>I've also been persuaded to start watching Arcane, and though I've only watched one and a half episodes so far I really like the art style!</p> <h2 id="food" tabindex="-1">Food</h2> <p>I absolutely haven't kept up any kind of recipe sharing habit recently because, well, life, but here are a few bangers I've cooked (relatively) recently:</p> <ul> <li><a href="https://www.bbc.co.uk/food/recipes/roast_chicken_with_48441">Roast chicken with tomatoey bulgar wheat</a> from &quot;Greekish&quot; by Georgina Hayden – this is <em>the</em> roast chicken recipe I will make for the rest of my life. The chicken isn't even the best bit. The bulgar wheat gets cooked in the juices, and is <em>incredible</em>. The whole book is great, but this is a standout.</li> <li><a href="https://www.maricruzavalos.com/cochinita-pibil-tacos/">Cochinita pibil</a> – great to feed a crowd, and absolutely divine. I bought a ton of tiny corn tortillas from <a href="https://www.mexgrocer.co.uk">mexgrocer.co.uk</a>.</li> <li>After 35 years I've finally decided I like aubergine, and it's all thanks to this <a href="https://www.saltsugarandi.com/2019/04/pasta-alla-norma.html">pasta alla Norma</a> recipe (good old Ottolenghi).</li> </ul> <h2 id="games" tabindex="-1">Games</h2> <p><a href="https://store.steampowered.com/app/230700/LaMulana/">La Mulana</a> is a fiendishly difficult <a href="https://en.wikipedia.org/wiki/Metroidvania">Metroidvania</a> puzzle platformer set in ancient ruins, where you play an archaeologist searching for the secret of La Mulana. I've never persevered with a game this much, ever, and managed to make it about 95% of the way through – beating some really hard bosses! – until I got to a really difficult horrible jumping puzzle over some spikes that kept killing me, and decided I couldn't be fucked. I really love a puzzle though, so still rate that game even though I had to play it with the <a href="https://steamcommunity.com/sharedfiles/filedetails/?id=377234096">hint guide</a> open because it really is impossible. Give me Metroidvania, secret passages and things hidden in plain sight, and I am guaranteed to be a fan. I've just picked up the sequel in the Steam autumn sale.</p> <p>I also really enjoyed <a href="https://www.inklestudios.com/heavensvault/">Heaven's Vault</a>: described as an &quot;archaeological science-fiction adventure game&quot; (all exciting words), you play Aliya, an archaeologist on the hunt for a missing roboticist in a strange futuristic nebula in space. You slowly build up a vocabulary of the language of the Ancients, and begin to understand more about what came before. I loved it so much I've put it on my <a href="https://localghost.dev/games">all-time favourites</a> list. Puzzles, adventure AND linguistics! I particularly loved starting to recognise different morphemes and understanding which ones indicated negation, verbs, nouns etc., so identifying the script became a lot easier as the game progressed.</p> <div class="content-grid"> <picture><source type="image/webp" srcset="https://localghost.dev/img/wQN3WvgY6v-280.webp 280w, https://localghost.dev/img/wQN3WvgY6v-640.webp 640w, https://localghost.dev/img/wQN3WvgY6v-960.webp 960w" sizes="auto"><img loading="lazy" decoding="async" src="https://localghost.dev/img/wQN3WvgY6v-280.jpeg" alt="Screenshot from Heaven's Vault: the player is invited to guess at the meaning of a word in the ancient script at the Base of the Giving Goddess." width="960" height="540" srcset="https://localghost.dev/img/wQN3WvgY6v-280.jpeg 280w, https://localghost.dev/img/wQN3WvgY6v-640.jpeg 640w, https://localghost.dev/img/wQN3WvgY6v-960.jpeg 960w" sizes="auto"></picture> <picture><source type="image/webp" srcset="https://localghost.dev/img/tsUEZMa8hW-280.webp 280w, https://localghost.dev/img/tsUEZMa8hW-640.webp 640w, https://localghost.dev/img/tsUEZMa8hW-960.webp 960w" sizes="auto"><img loading="lazy" decoding="async" src="https://localghost.dev/img/tsUEZMa8hW-280.jpeg" alt="The protagonist from Heaven's vault is a woman in a hijab with a nose piercing and a brown skin tone, She is walking through a desert surrounded by rocky cliffs, followed by a robot companion. She's saying 'there's something here..." width="960" height="540" srcset="https://localghost.dev/img/tsUEZMa8hW-280.jpeg 280w, https://localghost.dev/img/tsUEZMa8hW-640.jpeg 640w, https://localghost.dev/img/tsUEZMa8hW-960.jpeg 960w" sizes="auto"></picture> </div> <p>Finally got around to playing <a href="https://discoelysium.iam8bit.com/en-gb">Disco Elysium</a> several years after everyone else. It's a story-based game – it's pretty much entirely walking around, exploring and talking to people at length – but your dialogue choices and actions affect who you are as a character and how the other people around you perceive you. I'm being a good character because I am physically incapable of being an arsehole to people in video games, but I would like to play it again and be the absolutely unhinged lunatic. It's very funny and the world-building is incredible.</p> <h2 id="house" tabindex="-1">House</h2> <p>We've well and truly settled in to our house, and I love it so much. Especially now that we've nearly finished painting over the previous residents' questionable colour choices. I've got my own office, which has made working from home an absolute delight.</p> <div class="content-grid"> <a href="https://localghost.dev/img/blog/things-ive-been-enjoying/office-1.png"> <picture><source type="image/webp" srcset="https://localghost.dev/img/5W-eVla9jH-280.webp 280w, https://localghost.dev/img/5W-eVla9jH-640.webp 640w, https://localghost.dev/img/5W-eVla9jH-960.webp 960w" sizes="auto"><img loading="lazy" decoding="async" src="https://localghost.dev/img/5W-eVla9jH-280.jpeg" alt="A photograph of my office, painted a peachy-pink colour. There's a light green armchair with two cushions: a black abstract one and a beige one with stylised foxes on it. Behind it is a framed print of the Brick Lane Beigel Bake, and a riso print of a dog catching a frisbee in various positions. There's a tall grey-green IKEA drawer tower with a candle on top, next to a tatty desk with a covered sewing machine and a hand-painted advent calendar on it. Above the desk is a framed movie-poster from Beyond Tellerrand with the title 'this website is under construction - a love letter to the personal website'. There's a healthy looking monstera plant next to the desk, and behind that is a leaning bookshelf covered in trinkets and plants." width="960" height="720" srcset="https://localghost.dev/img/5W-eVla9jH-280.jpeg 280w, https://localghost.dev/img/5W-eVla9jH-640.jpeg 640w, https://localghost.dev/img/5W-eVla9jH-960.jpeg 960w" sizes="auto"></picture> </a> <a href="https://localghost.dev/img/blog/things-ive-been-enjoying/office-4.png"> <picture><source type="image/webp" srcset="https://localghost.dev/img/cC1k-oR0sK-280.webp 280w, https://localghost.dev/img/cC1k-oR0sK-640.webp 640w, https://localghost.dev/img/cC1k-oR0sK-960.webp 960w" sizes="auto"><img loading="lazy" decoding="async" src="https://localghost.dev/img/cC1k-oR0sK-280.jpeg" alt="A photo of my desk in the evening. I have a monitor with a lo-fi style purple and blue illustration of a telegraph pole, a desk fan, and a space themed desk mat with a pastel coloured keyboard on it. THere is a mug with some different types of computers on it. I have an Elgato key light air behind my monitor and there's a leather-bound notebook on the desk. Behind the monitor are some pictures (an illustration of the indo-european and uralic language families as a tree, two risograph prints of illustrations of the Barbican conservatory, and a framed print of a caterpillar that says' I do not care, I will write whatever I want and post it online') and a framed cross-stitch sampler featuring the international phonetic alphabet. Next to the desk is a set of framed prints that are just propped up on a set of drawers." width="960" height="720" srcset="https://localghost.dev/img/cC1k-oR0sK-280.jpeg 280w, https://localghost.dev/img/cC1k-oR0sK-640.jpeg 640w, https://localghost.dev/img/cC1k-oR0sK-960.jpeg 960w" sizes="auto"></picture> </a> </div> <p>It's amazing to have enough space to leave craft stuff out on its own table, and I've had a great time finding prints to put up on the walls. There's still a handful to put up but I'm waiting to find a big one to go in the middle of the gallery wall behind my monitor.</p> <h2 id="craft" tabindex="-1">Craft</h2> <p>Having space in my office for crafts means I've actually done a bit of sewing recently, though still haven't picked up dressmaking since the early days of the pandemic. A few of us in my choir decided to make a banner in the style of the <a href="https://www.artichoke.uk.com/project/women-making-history/">hand-sewn ones</a> women traditionally made for protests and campaigns (suffrage, peace camps, etc), that we could display at gigs and performances. After brainstorming a few ideas we decided to keep it simple and create a banner with our logo on it. I could probably tell you what podcasts I was listening to while painstakingly zig-zagging edges. It was a joint effort between about five of us, which makes it really special, and I think it came out really well.</p> <p>It absolutely needs an iron, but it takes a lot more than a blog post to persuade me to get the iron out.<br> <picture><source type="image/webp" srcset="https://localghost.dev/img/iQcgL5BPVV-280.webp 280w, https://localghost.dev/img/iQcgL5BPVV-640.webp 640w" sizes="auto"><img loading="lazy" decoding="async" src="https://localghost.dev/img/iQcgL5BPVV-280.jpeg" alt="A peach-coloured fabric banner with a navy blue fabric cassette tape appliquéd on top. It's emblazoned with the words 'MIXTAPE CHOIR'. It is horribly wrinkled." width="640" height="480" srcset="https://localghost.dev/img/iQcgL5BPVV-280.jpeg 280w, https://localghost.dev/img/iQcgL5BPVV-640.jpeg 640w" sizes="auto"></picture></p> <p>I also decided it'd be a GREAT idea to DIY an advent calendar this year. It is definitely not cheaper than buying one, but it was a fun little project. I'd had lofty ambitions of filling it with non-edible things, but it turns out that gets very expensive very quickly, so I gave up and it's mostly treats. I'm not the <em>greatest</em> painter, but good enough – I think it looks cute!<br> <picture><source type="image/webp" srcset="https://localghost.dev/img/PuK32pMDLV-280.webp 280w, https://localghost.dev/img/PuK32pMDLV-640.webp 640w" sizes="auto"><img loading="lazy" decoding="async" src="https://localghost.dev/img/PuK32pMDLV-280.jpeg" alt="A wooden advent calendar with 24 various-sized drawers. The drawers have been painted red, gold, silver, green and white, and decorated with christmas motifs such as trees, tinsel, wreaths, candy cane stripes and the smallest drawers have been painted to look like presents." width="640" height="853" srcset="https://localghost.dev/img/PuK32pMDLV-280.jpeg 280w, https://localghost.dev/img/PuK32pMDLV-640.jpeg 640w" sizes="auto"></picture></p> <h2 id="art" tabindex="-1">Art</h2> <p>A couple of months ago I went to see <em>Fragile Beauty</em>, the <a href="https://www.vam.ac.uk/exhibitions/fragile-beauty-photographs-from-the-sir-elton-john-and-david-furnish-collection">exhibition</a> of Elton John &amp; David Furnish's photography collection at the V&amp;A. I've always loved photography – specifically portrait and documentary photography – so this was a delight. And there was so much of it! I hadn't really known what to expect, but was amazed to see some prints of iconic works by David Avedon, Cindy Sherman and Nan Goldin amongst others. Each room had been expertly curated and I could've spent hours in there. It's on until early Jan 2025 but I think it's been doing a bit of a world tour, so if it pops up near you I highly recommend it.</p> <figure> <picture><source type="image/webp" srcset="https://localghost.dev/img/_ouyHkgcKc-280.webp 280w, https://localghost.dev/img/_ouyHkgcKc-640.webp 640w, https://localghost.dev/img/_ouyHkgcKc-960.webp 960w" sizes="auto"><img loading="lazy" decoding="async" src="https://localghost.dev/img/_ouyHkgcKc-280.jpeg" alt="Two drag queens sit in the back of a taxi. One of them is wearing a tight metallic tank sleeveless top and has a short, choppy blue wig; she is wearing large silver heart-shaped earrings, thick dark eyeshadow and deep crimson lipstick. The second drag queen is wearing a long-sleeved ripped white fishnet crop top under a metallic bra, with a curly blonde updo wig, very pale face powder with dark eyeshadow and dark red lips. The straps of her bra are hanging off her shoulders." width="960" height="660" srcset="https://localghost.dev/img/_ouyHkgcKc-280.jpeg 280w, https://localghost.dev/img/_ouyHkgcKc-640.jpeg 640w, https://localghost.dev/img/_ouyHkgcKc-960.jpeg 960w" sizes="auto"></picture> <figcaption><i>Misty and Jimmy Paulette in a taxi, NYC</i>, 1991. ©️ Nan Goldin </figcaption> </figure> <h2 id="books" tabindex="-1">Books</h2> <p>Continuing my habit of alternating a good book with a trash book. One grown-up book, one fantasy romance book. Like main course and dessert.</p> <p><a href="https://en.wikipedia.org/wiki/Children_of_Time_(novel)">Children of Time</a> by Adrian Tchaikovsky: a sci-fi set in the far future where we've gone and trashed earth so they're trying to populate a new planet with primates and some virus to fast-forward evolution, only it doesn't quite go to plan. Not for arachnophobes. The second one didn't grab me quite so much but I'll finish it at some point!</p> <p>I've been reading plenty of fantasy-romance books as well, like everyone else in my immediate sphere. I've ticked off some of the big series: Throne of Glass, Crescent City, From Blood and Ash, etc. They all blend into one at some point, but are still completely unputdownable. I read one recently called <a href="https://uk.bookshop.org/p/books/paladin-s-grace-t-kingfisher/7672899">Paladin's Grace</a> (by T. Kingfisher) which was quite a nice change: the characters were over the age of about 25, and the male protagonist was a paladin who likes to knit socks for people. It was a nice break from all the brooding and snarling.</p> <p>I also enjoyed <a href="https://uk.bookshop.org/p/books/yellowface-rebecca-f-kuang/6923283?ean=9780008532819">Yellowface</a> by Rebecca F Kuang – an excellent story of a white author who takes credit for her Chinese-American friend's work – and it took me <em>far</em> too long to realise she is also R.F. Kuang who wrote the brilliant <a href="https://uk.bookshop.org/p/books/babel-or-the-necessity-of-violence-an-arcane-history-of-the-oxford-translators-revolution-r-f-kuang/6627642?ean=9780008501853">Babel</a> (linguistics! magical realism! colonialism!). So, an author to follow.</p> <h2 id="garden" tabindex="-1">Garden</h2> <p>Finally, the garden! It's wonderful and immensely overwhelming having a garden – well, two actually, as we have a front garden as well – but we've made such a difference in it already. I swapped out the summer planter for a winter one I put together myself, featuring some gorgeous ornamental brassicas (the cabbagey plants), a spiky blue grass and some other foliage for some texture, and gorgeous berries for colour (&quot;Christmas cherry&quot; apparently, though highly toxic which is not very Christmassy!). I love it, and so far the slugs have stayed off.</p> <figure> <picture><source type="image/webp" srcset="https://localghost.dev/img/sh0XIiT8E0-280.webp 280w, https://localghost.dev/img/sh0XIiT8E0-640.webp 640w, https://localghost.dev/img/sh0XIiT8E0-960.webp 960w" sizes="auto"><img loading="lazy" decoding="async" src="https://localghost.dev/img/sh0XIiT8E0-280.jpeg" alt="A planter within a larger raised bed. It contains a bushy plant covered in bright orange and green tomato-like berries, a tall spiky blue grass, three cabbage-like ornamental brassicas that are purple and white in the middle, some mossy looking blue grass, a little bit of green foliage spraying out the front, a white cyclamen at the back" width="960" height="1280" srcset="https://localghost.dev/img/sh0XIiT8E0-280.jpeg 280w, https://localghost.dev/img/sh0XIiT8E0-640.jpeg 640w, https://localghost.dev/img/sh0XIiT8E0-960.jpeg 960w" sizes="auto"></picture> <figcaption>The rose stems are to dissuade foxes from digging up the newly-planted heucheras around it...</figcaption> </figure> The art in everyday life 2024-11-12T00:00:00Z https://localghost.dev/blog/the-art-in-everyday-life/ <p>I was very fortunate to speak at another excellent Beyond Tellerrand last week, alongside some brilliant and wonderful people. Once again I was inspired by the variety of topics and messages people shared on the stage. Alongside the usual artists’ inspiring career stories and techies’ (equally inspiring) web design magic, there was a strong undercurrent of anticapitalist messaging in a way that is more important than ever, especially relevant given the conference happened in the wake of a truly disastrous election result in the US.</p> <p>Talented illustrator and lovely human Paddy Donnelly <a href="https://beyondtellerrand.com/events/berlin-2024/speakers/paddy-donnelly#talk">spoke</a> about his career change from UX designer to children’s book author and illustrator. He reflected that before he changed career, he realised that everything he created was digital, and that he wanted to create a lasting legacy in the world – something that would be around long after he was.</p> <p>At this point I had one of my multiple (and regular) existential crises wondering whether I could really say the same about anything I do. Will there be anything that I can point to when I’m old and say “I did that”? (What is the point of anything, what am I doing with my life, etc etc.)</p> <p>Well, possibly not. I do software development for a bank. But I don’t actually think that matters very much. Because art is many things, and some of those things are transient. And I relish things that bring me joy in the present moment.</p> <p>(To be clear, I totally respect Paddy’s thinking there and our world is all the richer for the beautiful books he writes; it’s actually my own immediate existential crisis that I’m disagreeing with here.)</p> <p>I don’t necessarily believe that <em>everyone</em> can have a job or even a career that makes them spring out of bed in the morning and gives them creative satisfaction in their day-to-day. Ultimately we live under late-stage capitalism, and I certainly couldn’t afford my house (or indeed any house) if I dedicated my life to, say, running choirs. I massively respect anyone who can and does make a living from a creative art, and I thank you (and want to buy your wares). I work for a Tech Company doing Tech Things for Capitalism, but exercise my agency by being very selective about the companies I work for and the areas I want to work in. The company I work for may be a bank, but it’s changed the banking industry for the better and I believe in the company’s mission. A lot of the work I do is around improvements for customers and the customer service agents who serve them. It’s not children’s books, but it’s a living.</p> <p>So I get my creative energy and joy from elsewhere. I’m fiercely protective of my free time, even if it’s just spent horizontal on the sofa playing video games. When the working day is done, I have creative pursuits that bring me joy and put art into the world in their own little way.</p> <p>The wonderful <a href="https://hidde.blog">Hidde de Vries</a> stepped out of his usual role as the Pope of Popovers in his closing keynote <a href="https://talks.hiddedevries.nl/dFZf3b/creativity-cannot-be-computed">Creativity cannot be computed</a>. It was a love letter to art in all its forms, a look at what it represents, and what it can do.</p> <figure> <picture><source type="image/webp" srcset="https://localghost.dev/img/xH2bK2RRiF-280.webp 280w, https://localghost.dev/img/xH2bK2RRiF-640.webp 640w, https://localghost.dev/img/xH2bK2RRiF-960.webp 960w" sizes="auto"><img loading="lazy" decoding="async" src="https://localghost.dev/img/xH2bK2RRiF-280.jpeg" alt="Hidde de Vries on stage in front of a slide that says 'The artist's intention, reflection, research, skill, world view' with a right arrow pointing to the word 'art'" width="960" height="720" srcset="https://localghost.dev/img/xH2bK2RRiF-280.jpeg 280w, https://localghost.dev/img/xH2bK2RRiF-640.jpeg 640w, https://localghost.dev/img/xH2bK2RRiF-960.jpeg 960w" sizes="auto"></picture> <figcaption>Hidde at Beyond Tellerrand Berlin 2024</figcaption> </figure> <p>And it made me think some more. I should be broader in my definition of &quot;art&quot;. Anything remotely creative can be art, if you want it to be. Anything that moves you, or makes you feel something, or brings you joy.</p> <p>Art is in the songs I teach to my choir, the arrangements I’m proud of even if they only exist as mp3s on a Google Drive and as individual parts in the brains of a handful of exhausted Londoners. We’ll perform those songs, bring joy to the audience (and ourselves), and people might remember it, or they might not.</p> <p>Art is watching other people in the choir learn to arrange and conduct songs, their faces lighting up as the harmony comes together and the choir is brings their ideas to life.</p> <p>Art is in the weird and wonderful websites I make occasionally, which bring people (including myself!) a moment of joy when they land on them.</p> <p>Art is in the food I make. I’m a great cook (and an even messier one) and the Jewish side of my family has instilled in me a primal need to feed everybody. I show love through my food, so I bake for people, I mail out homemade sweets at Christmas, and I love feeding a crowd. Hours of preparation is gone in minutes, but it’s worth it.</p> <div class="content-grid"> <figure> <picture><source type="image/webp" srcset="https://localghost.dev/img/jFILzh-rc1-280.webp 280w, https://localghost.dev/img/jFILzh-rc1-640.webp 640w, https://localghost.dev/img/jFILzh-rc1-960.webp 960w" sizes="auto"><img loading="lazy" decoding="async" src="https://localghost.dev/img/jFILzh-rc1-280.jpeg" alt="Small knotted cardamom buns sit carefully posed on a wooden board. They're glazed, and dusted with sugar." width="960" height="720" srcset="https://localghost.dev/img/jFILzh-rc1-280.jpeg 280w, https://localghost.dev/img/jFILzh-rc1-640.jpeg 640w, https://localghost.dev/img/jFILzh-rc1-960.jpeg 960w" sizes="auto"></picture> <figcaption>Cardamom buns: recipe <a href="https://www.fixfeastflair.com/home/2015/2/9/swedish-cardamom-rolls-kardemummabullar-recipe">FixFeastFlair</a></figcaption> </figure> <figure> <picture><source type="image/webp" srcset="https://localghost.dev/img/N8K1q89TLF-280.webp 280w, https://localghost.dev/img/N8K1q89TLF-640.webp 640w, https://localghost.dev/img/N8K1q89TLF-960.webp 960w" sizes="auto"><img loading="lazy" decoding="async" src="https://localghost.dev/img/N8K1q89TLF-280.jpeg" alt="A pale yellow tart in a pastry case, with a sprinkling of black and white sesame seeds in a circle on top." width="960" height="720" srcset="https://localghost.dev/img/N8K1q89TLF-280.jpeg 280w, https://localghost.dev/img/N8K1q89TLF-640.jpeg 640w, https://localghost.dev/img/N8K1q89TLF-960.jpeg 960w" sizes="auto"></picture> <figcaption>Salted honey and sesame custard tart: recipe <a href="https://theguardian.com/food/2023/feb/24/salted-honey-sesame-custard-tart-recipe-benjamin-ebuehi">Benjamina Ebuehi</a></figcaption> </figure> </div> <p>Art is in the flowers I grow, which bring colour and beauty to the garden for a brief season before dying back, but will sustain the insects and birds that pollinate our plants and underpin our ecosystem.</p> <div class="content-grid"> <figure> <picture><source type="image/webp" srcset="https://localghost.dev/img/PXdYYrnrdL-280.webp 280w, https://localghost.dev/img/PXdYYrnrdL-640.webp 640w, https://localghost.dev/img/PXdYYrnrdL-960.webp 960w" sizes="auto"><img loading="lazy" decoding="async" src="https://localghost.dev/img/PXdYYrnrdL-280.jpeg" alt="A hot pink verbena with clusters of tiny flowers against a backgrop of green foliage, next to some sage and mint and a fern that needs dead leaves cutting off" width="960" height="1280" srcset="https://localghost.dev/img/PXdYYrnrdL-280.jpeg 280w, https://localghost.dev/img/PXdYYrnrdL-640.jpeg 640w, https://localghost.dev/img/PXdYYrnrdL-960.jpeg 960w" sizes="auto"></picture> <figcaption>Verbena Sissinghurst</figcaption> </figure> <figure> <picture><source type="image/webp" srcset="https://localghost.dev/img/phJB5kDaXt-280.webp 280w, https://localghost.dev/img/phJB5kDaXt-640.webp 640w, https://localghost.dev/img/phJB5kDaXt-960.webp 960w" sizes="auto"><img loading="lazy" decoding="async" src="https://localghost.dev/img/phJB5kDaXt-280.jpeg" alt="A blue-purple geranium flower with pink stripes and a centre that fades to white. It's set out against a backgrop of pointy geranium leaves." width="960" height="1280" srcset="https://localghost.dev/img/phJB5kDaXt-280.jpeg 280w, https://localghost.dev/img/phJB5kDaXt-640.jpeg 640w, https://localghost.dev/img/phJB5kDaXt-960.jpeg 960w" sizes="auto"></picture> <figcaption>Geranium Rozanne</figcaption> </figure> </div> <p>Art is in the little crafty projects I do, and sometimes finish, but usually don’t. I’m painting an advent calendar right now, lots of little wooden drawers, and I’m not the greatest painter but it’s going to be delightful, and full of chocolate anyway.</p> <p>I think there’s even a bit of art in shitposts and stupid jokes that I make knowing that people will roll their eyes but it gives me a little bit of joy to think about it. I learned from an early age that I could make people laugh. If I can distract you from... <em>all of this</em> for just a second with a really stupid joke, it was worth it. (That said, some of my jokes definitely deserve the withering looks.)</p> <p>All this to say, art can be fleeting and still be worth it. I encourage you to lean into the little things that bring you joy, and think about where there’s art in those things. You don’t have to paint the Sistine Chapel to bring art into the world.</p> <p>It’s becoming more important than ever that people keep making art, in the age of derivative AI slop and an ever-worsening political climate. Deliberately creative pursuits are radical. I gave a talk a while back about building personal websites (and I’ll write that talk up soon) and how that’s a radical act in this day and age of an internet of shit, and I think this is exactly the same. In a world of shit, creativity for creativity’s sake is radical.</p> Good links: 22 September 2024 2024-09-22T00:00:00Z https://localghost.dev/blog/good-links-2024-09-22/ <ul> <li><a href="https://www.nicchan.me/">Nic Chan</a> - Nic has just finished rebuilding her website and it’s INCREDIBLE! Seriously just look at it</li> <li><a href="https://css-irl.info/limitation-breeds-creativity/">CSS { In Real Life } | Limitation Breeds Creativity: A Study in Composition with Custom Properties</a> - Michelle has done some (characteristically) amazing things with a handful of divs.</li> <li><a href="https://www.bram.us/2024/09/14/introducing-bramus-caniuse-cli-a-cli-tool-for-can-i-use/">Introducing @bramus/caniuse-cli, a CLI tool for “Can I Use …”</a> - This is going to be very useful!</li> <li><a href="https://alistairshepherd.uk/writing/selling-web-project/">Selling a small front-end web project — what I learned - Alistair Shepherd</a> - “In mid-2024 I sold a small unmonetised web project of mine called My Top for Spotify. This is my experience of the sale and what I learned!”</li> <li><a href="https://shkspr.mobi/blog/2024/09/http-ftp-and-dict/">http:, ftp:, and ... dict:?</a> - Terence uncovers a long-forgotten protocol.</li> </ul> You should go to conferences 2024-09-16T00:00:00Z https://localghost.dev/blog/you-should-go-to-conferences/ <p>Those of you who know me (or who have been reading my posts for a while) will know that I'm often at conferences. I tend to speak at around four a year, plus attending one or two on top of that. I also know a great many people who never go to conferences, and I think they're missing out. Conferences are a fantastic way to not only broaden your horizons when it comes to your job and your skills, but also meet excellent people who might lead you to a new role, or new experiences.</p> <p>Through the talks I've seen over the past few years, I've learnt so much about what's possible in CSS nowadays, learnt from others' stories and experiences, got inspired to stick popovers everywhere, levelled up my knowledge of web performance, and so much more. As a developer, I also benefit from e.g. the UX and design talks at mixed conferences that give me more of an insight into how those processes work and how best practices evolve, because we don't and shouldn't work in silos.</p> <p>The talks are obviously very important, but one of the best things about conferences is the &quot;hallway track&quot; – that is, meeting and chatting to like-minded folks. Organisers will often encourage the <a href="https://www.ericholscher.com/blog/2017/aug/2/pacman-rule-conferences/">&quot;Pac-Man rule&quot;</a> - standing in a circle with a gap to always allow new people to join in. The afterparty is a great opportunity to socialise and meet people who do what you do. Even if you go to a conference with a group of people from your company, I encourage you to chat to others and see what other companies are up to. Maybe they're facing the same challenges as you are; maybe they've come up with a creative solution to something you've been thinking about. Maybe you've got something to teach them.</p> <p>It's obviously not a cheap thing to do (though some are not too expensive in the grand scheme of things), and especially if you're self-employed it can be a lot of money to spend. You can always ask folks who spoke at or attended particular conferences in previous years what they thought of the event to make sure it's worth your time.</p> <p>A lot of conferences also offer scholarship tickets to folks who wouldn't otherwise be able to attend for monetary reasons, as well as to encourage a more inclusive and diverse attendance.</p> <p>If you're not totally on board with in-person events, that's fine too – most if not all events these days have a livestream you can watch instead. There is usually a conference Slack or virtual platform where you can chat to other livestream attendees.</p> <h2 id="buy-your-tickets-early" tabindex="-1">Buy your tickets early</h2> <p>Since the pandemic, things have been a lot more challenging for conference organisers. Everything's more expensive, meaning the cost of running the events is higher. Companies have been tightening their belts and it becomes harder for attendees to get the budget for conference tickets. Organisers of conferences I've spoken at recently have reflected that people are buying tickets later and later, making it really unclear whether they're actually going to sell enough tickets to go ahead. Marc, who organises Beyond Tellerrand, <a href="https://marcthiele.com/notes/state-of-events-2024">wrote about this</a> earlier in the year. Though speakers generally invoice after the event, the money from ticket sales needs to be coming in earlier for organisers to be able to afford things like hotels, venue hire and catering. Sadly, <a href="https://frontconference.com">Front Conference</a> was unable to go ahead this year because they didn't sell enough tickets ahead of time. I was lucky enough to speak last year, and it was a fantastic event with some really incredible speakers.</p> <p>(It should be said here that some bigger, more commercial conferences are, well, extortionate: my learning budget is a generous £1000 a year but Lead Dev/StaffPlus London was so expensive that I actually had to beg and borrow a bit of someone else's learning budget to cover the ticket. I was too late for the early bird or group tickets for StaffPlus. It's a shame, because that's one of the conferences I get the most value from these days. But the majority of conferences I've seen – especially the community-run ones – are somewhat less pricey.)</p> <p>The other benefit of buying tickets earlier (besides, you know, the conference actually being able to take place) is that you can often take advantage of early bird discounts, getting your ticket cheaper than you would otherwise.</p> <p>Promoting conferences has also become much more difficult now that social media is so fragmented. Twitter had a great reach which just doesn't exist any more.</p> <h2 id="conferences-need-sponsors" tabindex="-1">Conferences need sponsors!</h2> <p>Conferences rely on corporate sponsorship to keep ticket prices reasonable. As well as venue hire (which can be super expensive), there's generally some kind of catering costs, speaker travel and accommodations (many conferences also pay their speakers an honorarium to reflect the hard work that goes into speaking), live captioning, streaming, AV, etc. There's usually an afterparty which will likely cost to hire as well.</p> <p>Generally conferences offer sponsorship packages of different tiers – for example, All Day Hey! offers a package that lets you sponsor the coffee for attendees, which is nice.</p> <p>If you can, consider asking your company to sponsor a conference. It's great for B2B companies to get the word out about their product, and it can be good for hiring as well!</p> <h2 id="some-of-my-favourite-smaller-web-conferences" tabindex="-1">Some of my favourite smaller web conferences</h2> <p>This is a living list (updated sporadically) based off of my experiences speaking and attending, as well as recommendations from some friendly folks on Mastodon. This list is by no means exhaustive, and if there's a conference you've attended that you really love, please let me know and I'll add it to the list with your personal recommendation!</p> <p>My vague price indicators:<br> £: up to £150<br> ££: £150-300<br> £££: £300-600<br> ££££: £600+</p> <p>Events with a range indicate early bird pricing vs full pricing.</p> <h3 id="uk" tabindex="-1">UK</h3> <ul> <li><a href="https://2024.stateofthebrowser.com/">State of the Browser</a> (London, web/accessibility, 1 day, £) An absolute bargain. PLUS you get to have coffee in the glorious Barbican conservatory.</li> <li><a href="https://ffconf.org">FFConf</a> (Brighton, web/UX, 1 day, ££). A family-run web event and general lovely vibes.</li> <li><a href="https://heypresents.com/conferences">All Day Hey!</a> (Leeds, web/design/UI/UX, 1 day, £). Similar vibes to FFConf, but with northern accents.</li> <li><a href="https://www.middlesbroughfe.co.uk/">Middlesbrough Frontend</a> (Middlesbrough, web/JS, 1 day, £). A newish conference which is small but mighty, and run by some very dedicated folks.</li> <li><a href="https://frontendnorth.com/">Front End North</a> (Sheffield, web, 1 day, TBC). Back for 2025! Organised by a small and lovely team, and guaranteed to be an excellent event.</li> <li><a href="https://pixelpioneers.co">Pixel Pioneers</a> (Bristol, web/design, 1 day, £-££). Small but perfectly formed (and carefully curated) conference on the lovely Bristol waterfront.</li> <li><a href="https://webdevconf.com/">WebDevConf</a> (Bristol, web, 1 day, £). Similar vibes to SOTB/All Day Hey (and in fact they are conference &quot;friends&quot;).</li> <li><a href="https://www.iosdevuk.com/">iOSDevUK</a> (Aberystwyth, iOS, 1 day + workshops extra, ££). Recommended by <a href="https://mastodon.social/@rhysmorgan/113147230457494469">Rhys</a>: &quot;My first iOS conference, and just so incredibly welcoming. I knew basically nobody going there, and met so many lovely people there last year – going back this year and seeing so many again was a delight.&quot;</li> </ul> <h3 id="europe" tabindex="-1">Europe</h3> <ul> <li><a href="https://beyondtellerrand.com/">Beyond Tellerrand</a> (Berlin/Düsseldorf, web/art/creativity/all sorts, 2 days + workshops extra, ££-£££). Two events a year, in Berlin and Düsseldorf. How does one even begin to describe Beyond Tellerrand? A melting pot of web, code, art, stories and experiences. I laughed, I cried. Check out my <a href="https://localghost.dev/blog/beyond-tellerrand-beyond-amazing">blog post</a> from Düsseldorf 2023.</li> <li><a href="https://cssday.nl">CSS Day</a> (Amsterdam, web, 2 days, ££££) I got invited to speak here in 2023, and it was absolutely mindblowing to see all the new developments in CSS. Awesome venue – an old church! – and excellent folks.</li> <li><a href="https://smashingconf.com">Smashing Conference</a> (Antwerp, design/UX; Freiburg, web/UI/UX; 2 days, £££) I just came back from Smashing Freiburg, and had a fantastic time. The organisers are some of the loveliest people I've ever known, and every talk was of an exceptional standard. What a great event, and a lovely setting too.</li> <li><a href="https://frontconference.com/">Front Conference</a> (Zurich, web/design/UX, 2 days, £££ at early bird price) – TBC if this returns for 2025. I really, really hope it does. Dual-track conference, one design/UX and one web/tech, with an excellently curated lineup. Zurich is a beautiful place, as well.</li> <li><a href="https://www.weyweyweb.com/">WeyWeyWeb</a> (Málaga, web, 2 days, ££ - group tickets available). The beautiful town of Málaga plays host to a small but lovingly put-together conference. A great chance to wear only a light jacket in November.</li> <li><a href="https://jsheroes.io/">JSHeroes</a> (Cluj-Napoca, Romania, web/JS, 2 days, £-££). I've not been myself, but I have it on good authority this is a lovely community-led event.</li> <li><a href="https://2025.pycon.it/en">PyCon Italia</a> (Bologna, Python, 3 days + workshops extra, TBC) Recommended by <a href="https://mastodon.social/@xahteiwi/113147243658830733">Florian</a>. They offer two-tier pricing so it's cheaper for folks who are paying their own way. Florian says: &quot;great community, terrific vibes, beautiful locations, <em>exceptional</em> speaker wrangling.&quot;</li> <li><a href="https://perfnow.nl/">PerfNow</a> (Amsterdam, web performance, 2 days, ££££) <a href="https://front-end.social/@sia/113147478766361053">Sia</a> says: &quot;Single track, one topic, great speakers, cool venue&quot;.</li> <li><a href="https://futurefrontend.com/">Future Frontend</a> (Espoo, Finland, web, 2 days, £££). <a href="https://github.com/sylwiavargas">Sylwia</a> says: &quot;The organizers put a lot of care into the speaker selection, and the sessions are grouped into thematic clusters which is interesting (so you have for example a block on green tech or music tech). There are lots of local speakers which is great. Everything is very well-organized and folks attending it are lovely. It’s a good conference to both learn a lot (it’s not devrel pitches), meet some nice folks, and enjoy the Finnish never-ending summer days.&quot;</li> <li><a href="https://nordicjs.com/">NordicJS</a> (Stockholm, web/JS) – <a href="https://tacocat.space/@jonas/113148550369570247">Jonas</a> says &quot;Nordic.js have been really fun in every year I’ve attended!&quot;</li> </ul> <h3 id="rest-of-world" tabindex="-1">Rest of world</h3> <ul> <li><a href="https://smashingconf.com">Smashing Conference</a> (New York, web/UX, 2 days + workshops extra, £££) See above!</li> <li><a href="https://jsconf.com">JSConf</a> (different countries) - I was lucky enough to go to the last CSSConf/JSconf EU in Berlin in 2019 and it was incredible. Lucky for us there are still a variety of JSConfs in different places including Budapest, Honolulu and Guadalajara!</li> <li><a href="https://www.clarityconf.com/">Clarity Conf</a> (online in PST, design systems, £££) – recommended by <a href="https://social.lol/@[email protected]/113147646286717715">Mike</a>.</li> <li><a href="https://fitc.ca/events/all/">FITC</a> (Toronto, design/content/UX, 2 days, ££££) recommended by <a href="https://social.lol/@[email protected]">Gregory</a></li> <li><a href="https://central.wordcamp.org/schedule/">WordCamp</a> (various locations, Wordpress, £) Community-run conferences for WordPress developers, worldwide, everywhere from Costa Rica to Kerala.</li> <li>??? – would love to hear of more conferences outside of Europe that I should be mentioning here!</li> </ul> New garden theme 2024-09-15T00:00:00Z https://localghost.dev/blog/new-garden-theme/ <p>I've built a new theme for this site, inspired by my love of gardening as well as one of my favourite video games.<br> With no pixel artistry skills to speak of, I'm grateful to the various artists over on <a href="http://itch.io">itch.io</a>, who I've credited on the <a href="https://localghost.dev/about">about</a> page, for all their lovely creations!</p> Good links: 15 September 2024 2024-09-15T00:00:00Z https://localghost.dev/blog/good-links-2024-09-15/ <ul> <li><a href="https://modem.io/blog/blog-monetization/">How to Monetize a Blog</a> - Just read it, ok? I promise it’s extremely worth it.</li> </ul> Good links: 18 August 2024 2024-08-18T00:00:00Z https://localghost.dev/blog/good-links-2024-08-18/ <ul> <li><a href="https://heydonworks.com/article/the-area-element/">The area element</a> - I’m really enjoying Heydon’s<br> HTML element safari, from the better-known to the lesser-known. This will certainly fall into the latter camp for a lot of folks!</li> </ul> liveness probe 2024-08-12T00:00:00Z https://localghost.dev/blog/liveness-probe/ <p>After an initial burst of blogging energy January followed by a series of automated posts featuring good things I’d read recently, it fell off a cliff towards the end of March. I didn't get bored, I promise!</p> <p>Life got extremely busy and reading through an ever-growing mountain of RSS feeds became too much of a chore. Eventually I declared bankruptcy and marked all as read, so if I’ve missed anything particularly excellent please do send it my way.</p> <p>The main thing contributing to aforementioned busyness: we bought a house! It’s great, we love it, we each have our own office which has completely transformed working from home for me, and means my husband’s sacred work space is safe from the human clutter tornado he’s married to.</p> <p>I’m now living out my gardening dreams, though those seem to involve more fighting shrubs than I’d anticipated.</p> <p>My sad little wind-battered acer is now loving life in a sheltered corner of the garden, and I’m hoping to put it in the ground. James is going to build some raised beds for a vegetable patch. I love going out there first thing and doing a bit of weeding, and it’s amazing to be able to just sit back on a deckchair in your own green space.</p> <p>We now have things we could only dream of when we were living a flat:</p> <ul> <li>a barbecue</li> <li>a fire pit</li> <li>outdoor furniture</li> <li>a dishwasher</li> <li>bifold doors</li> <li>room for a dog</li> <li>an endless wave of slugs and snails eating my plants</li> </ul> <p>We do have a lot of parakeets in the area, and so far the only bird visitors I’ve had have been pigeons and magpies. I put up a feeder but I don’t think there’s enough shelter for little birds to feel safe, so hopefully when the SHED I BOUGHT (!!! so excited) arrives it’ll be a bit more welcoming to our little winged friends.</p> <p>I can’t promise this won’t become a gardening blog.</p> <p>Outside of house, I’ve been adjusting to new responsibilities at work. I’ve been tech lead of my team for quite a while now but it’s grown a lot, so I’m adjusting to that plus leading big producty projects (as opposed to the more platformy projects we were doing before). It’s uncharted territory for me and I’ve done a lot of flapping, ranting and introspection to come to the conclusion that above anything else, I’m stressing myself out with my chaotic ways of working. I’m trying to be a bit more focussed and prioritise my time better (and get the hell off Slack). Impostor syndrome is repeatedly striking me over the head at the moment as I feel like any day people are going to realise that despite the hard-earned respect and reputation I’ve amassed over the last five years, I’m actually three children in a trench coat repeating “what are the tradeoffs”. I know it’ll get easier – it always does – but that doesn’t mean it’s not painful and difficult.</p> <p>I limited my conference speaking to four events again this year, finding that’s the magic number as long as I don’t have to spend half the year writing a load of different talks. I’ve been talking about technical migrations, and the lessons learned from the Typescript one we did at Monzo. I think it’s highly relatable, and the reception seems to be really good. I kicked off my conference year with All Day Hey in Leeds in May, followed by Pixel Pioneers in Bristol in June. Coming up I’m speaking at Smashing Freiburg in September, then Beyond Tellerrand Berlin in November. What an exciting list!</p> <p>On a side note, I was really sad to hear that Front conference had to cancel their event this year. I spoke there last year and it was really fantastic, with a great organising team. Marc Thiele, who runs Beyond Tellerrand, wrote a great blog post about the <a href="https://marcthiele.com/notes/state-of-events-2024">challenges of running conferences these days</a>.</p> <p>What else can I tell you? The last few months have been a blur of meetings and soil under my fingernails and endless paint samples.</p> <p>After four years’ service at Monzo I became eligible for a three-month paid sabbatical, and I’ve got the first month in September. I have a ridiculous electronics project lined up which you can be sure I’ll post about. I’m excited to have a bit of downtime!</p> Different ways to mock third-party integrations in Jest 2024-05-12T00:00:00Z https://localghost.dev/blog/different-ways-to-mock-third-party-integrations-in-jest/ <p>A few years back I got so bored of taking attendance for my choir that I wrote a Slack integration to do it for me (I got us on Slack in 2014). That evolved from a dodgy Python app triggered by slash commands into into a neat little Typescript suite called <a href="https://github.com/sophiekoonin/choirbot">Choirbot</a> that triggers via cron job. It does everything from posting reminders about upcoming rehearsals to reporting on attendance stats, and this weekend I added a new feature: the ability to nominate someone to facilitate the rehearsal if nobody else volunteers by 5pm the day of rehearsal.</p> <p>I don't touch the code very much, so every time I go back to look at it I have to spend ages figuring out how everything works all over again. You know why that is? Because I was lazy and didn't write any tests.</p> <p>You might think that a small side project isn't worth writing tests for, especially if it's just you contributing to it. But the odds of you breaking stuff increase exponentially the longer you leave it before adding a new feature, <em>especially</em> if that project involves libraries that get out of date, old versions of Node, and third party dependencies that involve arcane magic.</p> <p>So before I embarked upon building new functionality, I needed a full test suite that I could rely on to tell me if I'd broken anything, and that would show me a) what all the data structures look like and b) what the expected behaviour is across the whole app.</p> <p>My Choirbot app has three major integrations: <code>googleapis/google</code> to read from Google Sheets, <code>@slack/web-api</code> to communicate with Slack, and <code>@google-cloud/firestore</code> as a document store. It also reads from the <a href="https://gov.uk/bank-holidays.json">gov.uk Bank Holidays API</a>, still one of my favourite APIs ever due to its inclusion of whether bunting is appropriate for each holiday. I'd need to mock out all of these deps if I wanted to test my app thoroughly.</p> <p>I leaned heavily on Jest's <a href="https://jestjs.io/docs/manual-mocks">manual mocks</a> for all of these: library and file replacements stored in <code>__mocks__</code>.</p> <h2 id="mocking-the-slack-sdk" tabindex="-1">Mocking the Slack SDK</h2> <p>In my code wherever I'm interacting with the Slack API I'm actually doing it via a <a href="https://github.com/sophiekoonin/shebot/blob/76da79727c6ce977982e55c01db974aa2c659367/src/slack/__mocks__/client.ts">client</a> that I've instantiated and exported. So rather than mocking the Slack SDK directly, I can mock my client. The mock for this one, then, lives in the same directory as the client: <code>src/slack</code>.</p> <p>I've defined all of the Slack API functions I use in my code, and set them as <code>jest.fn()</code> so I can assert on what's being called and override the individual method return values as and when I need to. If I add a new Slack method call in somewhere, I'll need to add it here as well.</p> <pre class="language-typescript"><code class="language-typescript"><span class="token comment">// src/slack/__mocks__/client.ts</span> <span class="token keyword">export</span> <span class="token keyword">const</span> SlackClient <span class="token operator">=</span> <span class="token punctuation">{</span> chat<span class="token operator">:</span> <span class="token punctuation">{</span> postMessage<span class="token operator">:</span> jest<span class="token punctuation">.</span><span class="token function">fn</span><span class="token punctuation">(</span><span class="token punctuation">(</span><span class="token punctuation">)</span> <span class="token operator">=></span> <span class="token punctuation">{</span> <span class="token keyword">return</span> <span class="token punctuation">{</span> ok<span class="token operator">:</span> <span class="token boolean">true</span><span class="token punctuation">,</span> ts<span class="token operator">:</span> <span class="token string">'returnTimestamp'</span> <span class="token punctuation">}</span> <span class="token punctuation">}</span><span class="token punctuation">)</span><span class="token punctuation">,</span> update<span class="token operator">:</span> jest<span class="token punctuation">.</span><span class="token function">fn</span><span class="token punctuation">(</span><span class="token punctuation">)</span> <span class="token punctuation">}</span><span class="token punctuation">,</span> conversations<span class="token operator">:</span> <span class="token punctuation">{</span> history<span class="token operator">:</span> jest<span class="token punctuation">.</span><span class="token function">fn</span><span class="token punctuation">(</span><span class="token punctuation">)</span> <span class="token punctuation">}</span><span class="token punctuation">,</span> reactions<span class="token operator">:</span> <span class="token punctuation">{</span> add<span class="token operator">:</span> jest<span class="token punctuation">.</span><span class="token function">fn</span><span class="token punctuation">(</span><span class="token punctuation">)</span><span class="token punctuation">,</span> get<span class="token operator">:</span> jest<span class="token punctuation">.</span><span class="token function">fn</span><span class="token punctuation">(</span><span class="token punctuation">(</span><span class="token punctuation">)</span> <span class="token operator">=></span> <span class="token punctuation">{</span> <span class="token keyword">return</span> <span class="token punctuation">{</span> ok<span class="token operator">:</span> <span class="token boolean">true</span><span class="token punctuation">,</span> message<span class="token operator">:</span> <span class="token punctuation">{</span> reactions<span class="token operator">:</span> <span class="token punctuation">[</span> <span class="token punctuation">{</span> name<span class="token operator">:</span> <span class="token string">'thumbsup'</span><span class="token punctuation">,</span> count<span class="token operator">:</span> <span class="token number">1</span> <span class="token punctuation">}</span><span class="token punctuation">,</span> <span class="token punctuation">{</span> name<span class="token operator">:</span> <span class="token string">'thumbsdown'</span><span class="token punctuation">,</span> count<span class="token operator">:</span> <span class="token number">1</span> <span class="token punctuation">}</span> <span class="token punctuation">]</span> <span class="token punctuation">}</span> <span class="token punctuation">}</span> <span class="token punctuation">}</span><span class="token punctuation">)</span> <span class="token punctuation">}</span><span class="token punctuation">,</span> users<span class="token operator">:</span> <span class="token punctuation">{</span> list<span class="token operator">:</span> jest<span class="token punctuation">.</span><span class="token function">fn</span><span class="token punctuation">(</span><span class="token punctuation">)</span> <span class="token punctuation">}</span><span class="token punctuation">,</span> channels<span class="token operator">:</span> <span class="token punctuation">{</span> join<span class="token operator">:</span> jest<span class="token punctuation">.</span><span class="token function">fn</span><span class="token punctuation">(</span><span class="token punctuation">)</span> <span class="token punctuation">}</span><span class="token punctuation">,</span> views<span class="token operator">:</span> <span class="token punctuation">{</span> open<span class="token operator">:</span> jest<span class="token punctuation">.</span><span class="token function">fn</span><span class="token punctuation">(</span><span class="token punctuation">)</span><span class="token punctuation">,</span> publish<span class="token operator">:</span> jest<span class="token punctuation">.</span><span class="token function">fn</span><span class="token punctuation">(</span><span class="token punctuation">)</span><span class="token punctuation">,</span> update<span class="token operator">:</span> jest<span class="token punctuation">.</span><span class="token function">fn</span><span class="token punctuation">(</span><span class="token punctuation">)</span> <span class="token punctuation">}</span><span class="token punctuation">,</span> oauth<span class="token operator">:</span> <span class="token punctuation">{</span> v2<span class="token operator">:</span> <span class="token punctuation">{</span> access<span class="token operator">:</span> jest<span class="token punctuation">.</span><span class="token function">fn</span><span class="token punctuation">(</span><span class="token punctuation">)</span> <span class="token punctuation">}</span> <span class="token punctuation">}</span> <span class="token punctuation">}</span></code></pre> <p>Since these are all <code>jest.fn()</code> mocks, I can override the individual return types of each function in a test using <code>mockResolvedValue</code>:</p> <pre class="language-typescript"><code class="language-typescript"><span class="token keyword">import</span> <span class="token punctuation">{</span> SlackClient <span class="token punctuation">}</span> <span class="token keyword">from</span> <span class="token string">'../slack/client'</span> jest<span class="token punctuation">.</span><span class="token function">mock</span><span class="token punctuation">(</span><span class="token string">'../slack/client'</span><span class="token punctuation">)</span> <span class="token punctuation">[</span><span class="token operator">...</span><span class="token punctuation">]</span> <span class="token function">test</span><span class="token punctuation">(</span><span class="token template-string"><span class="token template-punctuation string">`</span><span class="token string">Persists the user ID of the person who volunteered and doesn't post</span><span class="token template-punctuation string">`</span></span><span class="token punctuation">,</span> <span class="token keyword">async</span> <span class="token punctuation">(</span><span class="token punctuation">)</span> <span class="token operator">=></span> <span class="token punctuation">{</span> SlackClient<span class="token punctuation">.</span>reactions<span class="token punctuation">.</span>get<span class="token punctuation">.</span><span class="token function">mockResolvedValue</span><span class="token punctuation">(</span><span class="token punctuation">{</span> channel<span class="token operator">:</span> <span class="token string">'test-channel'</span><span class="token punctuation">,</span> ok<span class="token operator">:</span> <span class="token boolean">true</span><span class="token punctuation">,</span> message<span class="token operator">:</span> <span class="token punctuation">{</span> reactions<span class="token operator">:</span> <span class="token punctuation">[</span> <span class="token punctuation">{</span> users<span class="token operator">:</span> <span class="token punctuation">[</span>testUserId<span class="token punctuation">]</span><span class="token punctuation">,</span> name<span class="token operator">:</span> <span class="token string">'raised_hands'</span> <span class="token punctuation">}</span><span class="token punctuation">,</span> <span class="token punctuation">{</span> users<span class="token operator">:</span> <span class="token punctuation">[</span>testUser2<span class="token punctuation">,</span> testUser3<span class="token punctuation">]</span><span class="token punctuation">,</span> name<span class="token operator">:</span> <span class="token string">'-1'</span> <span class="token punctuation">}</span> <span class="token punctuation">]</span> <span class="token punctuation">}</span> <span class="token punctuation">}</span><span class="token punctuation">)</span> <span class="token comment">// [rest of test logic]</span> <span class="token punctuation">}</span><span class="token punctuation">)</span> </code></pre> <p><a href="https://github.com/sophiekoonin/choirbot/blob/731491bf8787fc381cbe9379f90f66b584308cda/src/rehearsals/rehearsals.test.ts">See full test</a></p> <h2 id="mocking-the-google-sheets-sdk" tabindex="-1">Mocking the Google Sheets SDK</h2> <p>This <em>is</em> a mock for an external library, so it lives in <code>src/__mocks__</code> at the top level.</p> <p>I used <code>jest.createMockFromModule</code> to auto-generate a mock based on the whole module, and then overwrote its <code>google</code> export with my own mock.</p> <p>I wanted to be able to change the return value of the <code>batchGet</code> function to test different behaviour, but unlike with the Slack SDK, I can't use <code>mockResolvedValue</code> here because <code>google.sheets</code> is a function that returns an instance of the sheets client. Instead, to make sure the mock instance has the mock data I want it to, I've added a variable <code>mockBatchGetReturnValue</code> which I've added a setter for and exported as part of the mock.</p> <pre class="language-typescript"><code class="language-typescript"><span class="token comment">// src/__mocks__/googleapis.ts</span> <span class="token keyword">import</span> <span class="token punctuation">{</span> GoogleApis <span class="token punctuation">}</span> <span class="token keyword">from</span> <span class="token string">'googleapis'</span> <span class="token keyword">import</span> <span class="token punctuation">{</span> spreadsheetDateRows<span class="token punctuation">,</span> testSpreadsheetData <span class="token punctuation">}</span> <span class="token keyword">from</span> <span class="token string">'../test/testData'</span> <span class="token keyword">const</span> googleapis <span class="token operator">=</span> jest<span class="token punctuation">.</span><span class="token function">createMockFromModule</span><span class="token punctuation">(</span><span class="token string">'googleapis'</span><span class="token punctuation">)</span> <span class="token keyword">as</span> GoogleApis <span class="token keyword">let</span> mockBatchGetReturnValue <span class="token operator">=</span> testSpreadsheetData <span class="token keyword">export</span> <span class="token keyword">const</span> google <span class="token operator">=</span> <span class="token punctuation">{</span> auth<span class="token operator">:</span> <span class="token punctuation">{</span> getClient<span class="token operator">:</span> jest<span class="token punctuation">.</span><span class="token function">fn</span><span class="token punctuation">(</span><span class="token punctuation">)</span> <span class="token punctuation">}</span><span class="token punctuation">,</span> sheets<span class="token operator">:</span> jest<span class="token punctuation">.</span><span class="token function">fn</span><span class="token punctuation">(</span><span class="token punctuation">(</span><span class="token punctuation">)</span> <span class="token operator">=></span> <span class="token punctuation">(</span><span class="token punctuation">{</span> spreadsheets<span class="token operator">:</span> <span class="token punctuation">{</span> values<span class="token operator">:</span> <span class="token punctuation">{</span> get<span class="token operator">:</span> jest<span class="token punctuation">.</span><span class="token function">fn</span><span class="token punctuation">(</span><span class="token keyword">async</span> <span class="token punctuation">(</span><span class="token punctuation">)</span> <span class="token operator">=></span> <span class="token punctuation">{</span> <span class="token keyword">return</span> <span class="token punctuation">{</span> data<span class="token operator">:</span> <span class="token punctuation">{</span> values<span class="token operator">:</span> <span class="token punctuation">[</span>spreadsheetDateRows<span class="token punctuation">]</span> <span class="token punctuation">}</span> <span class="token punctuation">}</span> <span class="token punctuation">}</span><span class="token punctuation">)</span><span class="token punctuation">,</span> batchGet<span class="token operator">:</span> jest<span class="token punctuation">.</span><span class="token function">fn</span><span class="token punctuation">(</span><span class="token keyword">async</span> <span class="token punctuation">(</span><span class="token punctuation">)</span> <span class="token operator">=></span> <span class="token punctuation">{</span> <span class="token keyword">return</span> <span class="token punctuation">{</span> data<span class="token operator">:</span> <span class="token punctuation">{</span> valueRanges<span class="token operator">:</span> mockBatchGetReturnValue <span class="token punctuation">}</span> <span class="token punctuation">}</span> <span class="token punctuation">}</span><span class="token punctuation">)</span> <span class="token punctuation">}</span> <span class="token punctuation">}</span> <span class="token punctuation">}</span><span class="token punctuation">)</span><span class="token punctuation">,</span> <span class="token function-variable function">setMockBatchGetReturnValue</span><span class="token operator">:</span> <span class="token punctuation">(</span>value<span class="token operator">:</span> <span class="token keyword">typeof</span> mockBatchGetReturnValue<span class="token punctuation">)</span> <span class="token operator">=></span> <span class="token punctuation">{</span> mockBatchGetReturnValue <span class="token operator">=</span> value <span class="token punctuation">}</span><span class="token punctuation">)</span> <span class="token punctuation">}</span> <span class="token keyword">export</span> <span class="token keyword">default</span> googleapis</code></pre> <p>Example usage of <code>setMockBatchGetReturnValue</code> in a test:</p> <pre class="language-typescript"><code class="language-typescript"><span class="token keyword">import</span> <span class="token punctuation">{</span> google <span class="token punctuation">}</span> <span class="token keyword">from</span> <span class="token string">'googleapis'</span> jest<span class="token punctuation">.</span><span class="token function">mock</span><span class="token punctuation">(</span><span class="token string">'googleapis'</span><span class="token punctuation">)</span> <span class="token function">test</span><span class="token punctuation">(</span><span class="token string">'Posts a message if rehearsal is cancelled'</span><span class="token punctuation">,</span> <span class="token keyword">async</span> <span class="token punctuation">(</span><span class="token punctuation">)</span> <span class="token operator">=></span> <span class="token punctuation">{</span> <span class="token comment">// @ts-expect-error mock type</span> <span class="token class-name">google</span><span class="token punctuation">.</span><span class="token function">setMockBatchGetReturnValue</span><span class="token punctuation">(</span><span class="token punctuation">[</span> <span class="token punctuation">{</span> range<span class="token operator">:</span> <span class="token string">'B1:I1'</span><span class="token punctuation">,</span> values<span class="token operator">:</span> <span class="token punctuation">[</span>testSpreadsheetHeaders<span class="token punctuation">]</span> <span class="token punctuation">}</span><span class="token punctuation">,</span> <span class="token punctuation">{</span> range<span class="token operator">:</span> <span class="token string">'B4:I4'</span><span class="token punctuation">,</span> values<span class="token operator">:</span> <span class="token punctuation">[</span> <span class="token string">'Rehearsal cancelled'</span><span class="token punctuation">,</span> <span class="token string">'Run Through Title'</span><span class="token punctuation">,</span> <span class="token string">'Blah blah blah'</span><span class="token punctuation">,</span> <span class="token string">'main-song-link'</span><span class="token punctuation">,</span> <span class="token string">'run-through-link'</span> <span class="token punctuation">]</span> <span class="token punctuation">}</span> <span class="token punctuation">]</span><span class="token punctuation">)</span> <span class="token comment">// [rest of test]</span> <span class="token punctuation">}</span><span class="token punctuation">)</span></code></pre> <p><a href="https://github.com/sophiekoonin/choirbot/blob/731491bf8787fc381cbe9379f90f66b584308cda/src/rehearsals/rehearsals.test.ts">See full test</a></p> <h2 id="mocking-google-cloud-firestore" tabindex="-1">Mocking Google Cloud firestore</h2> <p>This took AGES to figure out. I had found <a href="https://github.com/sbatson5/firestore-jest-mock"><code>firestore-jest-mock</code></a> and thought it'd solve all my problems, but I found that it just didn't work when I used it in the way the <a href="https://github.com/sbatson5/firestore-jest-mock?tab=readme-ov-file#google-cloudfirestore-compatibility">docs recommended</a>. The tests just timed out, which suggested they were still trying to connect to the real database.</p> <p>I ended up going with the manual mock approach again, but exporting a class that had an instance of the Firestore stub from <code>firestore-jest-mock</code>. In order to change the DB data between tests, I had to reinstantiate the stub. Everything starts with the <code>collection()</code> function, so it was enough to just expose that function which then allows you to chain functions on the underlying stub.</p> <p>Like with Slack, my database instance is exported from a file, so I could mock that file instead of the entire SDK.</p> <p>I created a load of reusable test data, and imported that to use in the mock. I stored that elsewhere so I could import it in tests as well.</p> <pre class="language-typescript"><code class="language-typescript"><span class="token comment">// src/db/__mocks__/db.ts</span> <span class="token keyword">import</span> <span class="token punctuation">{</span> firestoreStub <span class="token punctuation">}</span> <span class="token keyword">from</span> <span class="token string">'firestore-jest-mock/mocks/googleCloudFirestore'</span> <span class="token keyword">import</span> <span class="token punctuation">{</span> Firestore <span class="token keyword">as</span> FirestoreT <span class="token punctuation">}</span> <span class="token keyword">from</span> <span class="token string">'@google-cloud/firestore'</span> <span class="token keyword">import</span> <span class="token punctuation">{</span> testAttendancePost<span class="token punctuation">,</span> testTeamData<span class="token punctuation">,</span> testTeamId <span class="token punctuation">}</span> <span class="token keyword">from</span> <span class="token string">'../../test/testData'</span> <span class="token keyword">type</span> <span class="token class-name">TestDataOverrides</span> <span class="token operator">=</span> <span class="token punctuation">{</span> teamOverrides<span class="token operator">?</span><span class="token operator">:</span> Partial<span class="token operator">&lt;</span><span class="token keyword">typeof</span> testTeamData<span class="token operator">></span> attendanceOverrides<span class="token operator">?</span><span class="token operator">:</span> Partial<span class="token operator">&lt;</span><span class="token keyword">typeof</span> testAttendancePost<span class="token operator">></span> attendance<span class="token operator">?</span><span class="token operator">:</span> <span class="token builtin">Array</span><span class="token operator">&lt;</span><span class="token keyword">typeof</span> testAttendancePost<span class="token operator">></span> teams<span class="token operator">?</span><span class="token operator">:</span> <span class="token builtin">Array</span><span class="token operator">&lt;</span><span class="token keyword">typeof</span> testTeamData<span class="token operator">></span> <span class="token punctuation">}</span> <span class="token keyword">class</span> <span class="token class-name"><span class="token constant">DB</span></span> <span class="token punctuation">{</span> mockFirestore<span class="token operator">:</span> FirestoreT <span class="token function">constructor</span><span class="token punctuation">(</span><span class="token punctuation">)</span> <span class="token punctuation">{</span> <span class="token keyword">const</span> <span class="token punctuation">{</span> Firestore <span class="token punctuation">}</span> <span class="token operator">=</span> <span class="token function">firestoreStub</span><span class="token punctuation">(</span><span class="token punctuation">{</span> database<span class="token operator">:</span> <span class="token punctuation">{</span> teams<span class="token operator">:</span> <span class="token punctuation">[</span>testTeamData<span class="token punctuation">]</span><span class="token punctuation">,</span> <span class="token punctuation">[</span><span class="token template-string"><span class="token template-punctuation string">`</span><span class="token string">attendance-</span><span class="token interpolation"><span class="token interpolation-punctuation punctuation">${</span>testTeamId<span class="token interpolation-punctuation punctuation">}</span></span><span class="token template-punctuation string">`</span></span><span class="token punctuation">]</span><span class="token operator">:</span> <span class="token punctuation">[</span>testAttendancePost<span class="token punctuation">]</span> <span class="token punctuation">}</span> <span class="token punctuation">}</span><span class="token punctuation">)</span> <span class="token keyword">this</span><span class="token punctuation">.</span>mockFirestore <span class="token operator">=</span> <span class="token keyword">new</span> <span class="token class-name">Firestore</span><span class="token punctuation">(</span><span class="token punctuation">)</span> <span class="token punctuation">}</span> <span class="token function">collection</span><span class="token punctuation">(</span>args<span class="token operator">:</span> <span class="token builtin">string</span><span class="token punctuation">)</span> <span class="token punctuation">{</span> <span class="token keyword">return</span> <span class="token keyword">this</span><span class="token punctuation">.</span>mockFirestore<span class="token punctuation">.</span><span class="token function">collection</span><span class="token punctuation">(</span>args<span class="token punctuation">)</span> <span class="token punctuation">}</span> <span class="token function">setMockDbContents</span><span class="token punctuation">(</span>testData<span class="token operator">:</span> TestDataOverrides<span class="token punctuation">)</span> <span class="token punctuation">{</span> <span class="token keyword">const</span> <span class="token punctuation">{</span> attendance<span class="token punctuation">,</span> teams<span class="token punctuation">,</span> teamOverrides<span class="token punctuation">,</span> attendanceOverrides <span class="token punctuation">}</span> <span class="token operator">=</span> testData <span class="token keyword">const</span> teamData <span class="token operator">=</span> <span class="token punctuation">{</span> <span class="token operator">...</span>testTeamData<span class="token punctuation">,</span> <span class="token operator">...</span>teamOverrides <span class="token punctuation">}</span> <span class="token keyword">const</span> attendanceData <span class="token operator">=</span> <span class="token punctuation">{</span> <span class="token operator">...</span>testAttendancePost<span class="token punctuation">,</span> <span class="token operator">...</span>attendanceOverrides <span class="token punctuation">}</span> <span class="token keyword">const</span> <span class="token punctuation">{</span> Firestore <span class="token punctuation">}</span> <span class="token operator">=</span> <span class="token function">firestoreStub</span><span class="token punctuation">(</span><span class="token punctuation">{</span> database<span class="token operator">:</span> <span class="token punctuation">{</span> teams<span class="token operator">:</span> teams <span class="token operator">?</span> teams <span class="token operator">:</span> <span class="token punctuation">[</span>teamData<span class="token punctuation">]</span><span class="token punctuation">,</span> <span class="token punctuation">[</span><span class="token template-string"><span class="token template-punctuation string">`</span><span class="token string">attendance-</span><span class="token interpolation"><span class="token interpolation-punctuation punctuation">${</span>testTeamId<span class="token interpolation-punctuation punctuation">}</span></span><span class="token template-punctuation string">`</span></span><span class="token punctuation">]</span><span class="token operator">:</span> attendance <span class="token operator">?</span> attendance <span class="token operator">:</span> <span class="token punctuation">[</span>attendanceData<span class="token punctuation">]</span> <span class="token punctuation">}</span> <span class="token punctuation">}</span><span class="token punctuation">)</span> <span class="token keyword">this</span><span class="token punctuation">.</span>mockFirestore <span class="token operator">=</span> <span class="token keyword">new</span> <span class="token class-name">Firestore</span><span class="token punctuation">(</span><span class="token punctuation">)</span> <span class="token punctuation">}</span> <span class="token punctuation">}</span> <span class="token keyword">const</span> testDB <span class="token operator">=</span> <span class="token keyword">new</span> <span class="token class-name"><span class="token constant">DB</span></span><span class="token punctuation">(</span><span class="token punctuation">)</span> <span class="token keyword">export</span> <span class="token keyword">const</span> db <span class="token operator">=</span> testDB</code></pre> <p>With <code>setMockDbContents</code> I added the ability to set overrides of individual data fields, or overwrite the entire table contents. I could then call <code>db.setMockDbContents</code> in a test when I needed to change what the database getters returned:</p> <pre class="language-typescript"><code class="language-typescript"> <span class="token keyword">import</span> <span class="token punctuation">{</span> db <span class="token punctuation">}</span> <span class="token keyword">from</span> <span class="token string">'../db/db'</span> jest<span class="token punctuation">.</span><span class="token function">mock</span><span class="token punctuation">(</span><span class="token string">'../db/db'</span><span class="token punctuation">)</span> <span class="token punctuation">[</span><span class="token operator">...</span><span class="token punctuation">]</span> <span class="token function">test</span><span class="token punctuation">(</span><span class="token string">"Messages the person who installed if couldn't find a post"</span><span class="token punctuation">,</span> <span class="token keyword">async</span> <span class="token punctuation">(</span><span class="token punctuation">)</span> <span class="token operator">=></span> <span class="token punctuation">{</span> db<span class="token punctuation">.</span><span class="token function">setMockDbContents</span><span class="token punctuation">(</span><span class="token punctuation">{</span> attendance<span class="token operator">:</span> <span class="token punctuation">[</span><span class="token punctuation">]</span> <span class="token punctuation">}</span><span class="token punctuation">)</span> <span class="token keyword">await</span> <span class="token function">updateAttendanceMessage</span><span class="token punctuation">(</span><span class="token punctuation">{</span> token<span class="token punctuation">,</span> teamId <span class="token punctuation">}</span><span class="token punctuation">)</span> <span class="token function">expect</span><span class="token punctuation">(</span>SlackClient<span class="token punctuation">.</span>chat<span class="token punctuation">.</span>postMessage<span class="token punctuation">)</span><span class="token punctuation">.</span><span class="token function">toHaveBeenCalledWith</span><span class="token punctuation">(</span><span class="token punctuation">{</span> token<span class="token punctuation">,</span> channel<span class="token operator">:</span> testUserId<span class="token punctuation">,</span> text<span class="token operator">:</span> <span class="token string">"Tried to update attendance message, but couldn't find the post to update."</span> <span class="token punctuation">}</span><span class="token punctuation">)</span> <span class="token punctuation">}</span><span class="token punctuation">)</span></code></pre> <p><a href="https://github.com/sophiekoonin/shebot/blob/e119f4d73329c8b1b234af44f56c31eeeb50c96f/src/attendance/attendance.test.ts">See full test</a></p> <h3 id="mocking-node-s-native-fetch-with-nock" tabindex="-1">Mocking Node's native <code>fetch</code> with nock</h3> <p>Since Node 18, <code>fetch</code> has been supported natively in Node! <a href="https://github.com/nock/nock"><code>nock</code></a> is a HTTP mocking library for Node and version 14 (currently in beta at the time of writing) includes support for native <code>fetch</code>. While many API calls may work in a test, it speeds things up a lot to mock them out.</p> <pre class="language-typescript"><code class="language-typescript"><span class="token keyword">import</span> nock <span class="token keyword">from</span> <span class="token string">'nock'</span> <span class="token keyword">import</span> <span class="token punctuation">{</span> isBankHoliday <span class="token punctuation">}</span> <span class="token keyword">from</span> <span class="token string">'./utils'</span> <span class="token function">nock</span><span class="token punctuation">(</span><span class="token string">'https://www.gov.uk'</span><span class="token punctuation">)</span> <span class="token punctuation">.</span><span class="token function">get</span><span class="token punctuation">(</span><span class="token string">'/bank-holidays.json'</span><span class="token punctuation">)</span> <span class="token punctuation">.</span><span class="token function">reply</span><span class="token punctuation">(</span><span class="token number">200</span><span class="token punctuation">,</span> <span class="token punctuation">{</span> <span class="token string-property property">'england-and-wales'</span><span class="token operator">:</span> <span class="token punctuation">{</span> events<span class="token operator">:</span> <span class="token punctuation">[</span> <span class="token punctuation">{</span> title<span class="token operator">:</span> <span class="token string">'New Year’s Day'</span><span class="token punctuation">,</span> date<span class="token operator">:</span> <span class="token string">'2024-01-01'</span><span class="token punctuation">,</span> notes<span class="token operator">:</span> <span class="token string">''</span><span class="token punctuation">,</span> bunting<span class="token operator">:</span> <span class="token boolean">true</span> <span class="token punctuation">}</span> <span class="token punctuation">]</span> <span class="token punctuation">}</span> <span class="token punctuation">}</span><span class="token punctuation">)</span> <span class="token function">describe</span><span class="token punctuation">(</span><span class="token string">'general utils'</span><span class="token punctuation">,</span> <span class="token punctuation">(</span><span class="token punctuation">)</span> <span class="token operator">=></span> <span class="token punctuation">{</span> <span class="token function">test</span><span class="token punctuation">(</span><span class="token string">'isBankHoliday'</span><span class="token punctuation">,</span> <span class="token keyword">async</span> <span class="token punctuation">(</span><span class="token punctuation">)</span> <span class="token operator">=></span> <span class="token punctuation">{</span> <span class="token function">expect</span><span class="token punctuation">(</span><span class="token keyword">await</span> <span class="token function">isBankHoliday</span><span class="token punctuation">(</span><span class="token string">'2024-01-01'</span><span class="token punctuation">)</span><span class="token punctuation">)</span><span class="token punctuation">.</span><span class="token function">toBe</span><span class="token punctuation">(</span><span class="token boolean">true</span><span class="token punctuation">)</span> <span class="token function">expect</span><span class="token punctuation">(</span><span class="token keyword">await</span> <span class="token function">isBankHoliday</span><span class="token punctuation">(</span><span class="token string">'2024-01-02'</span><span class="token punctuation">)</span><span class="token punctuation">)</span><span class="token punctuation">.</span><span class="token function">toBe</span><span class="token punctuation">(</span><span class="token boolean">false</span><span class="token punctuation">)</span> <span class="token punctuation">}</span><span class="token punctuation">)</span> <span class="token punctuation">}</span><span class="token punctuation">)</span> </code></pre> <p><a href="https://github.com/sophiekoonin/shebot/blob/638c1778e071fcc3a74ae7d387afe4e163d5404b/src/utils.test.ts">See full test</a></p> <p>A note here: if you're using <code>jest.useFakeTimers</code> it may cause the request mocked with <code>nock</code> to time out. I found a <a href="https://github.com/nock/nock/issues/2200">relevant GitHub issue</a> that suggested excluding a couple of timing functions from <code>useFakeTimers</code> as a workaround until the problem is fixed:</p> <pre class="language-js"><code class="language-js"> jest<span class="token punctuation">.</span><span class="token function">useFakeTimers</span><span class="token punctuation">(</span><span class="token punctuation">{</span> <span class="token literal-property property">doNotFake</span><span class="token operator">:</span> <span class="token punctuation">[</span><span class="token string">'nextTick'</span><span class="token punctuation">,</span> <span class="token string">'setImmediate'</span><span class="token punctuation">]</span> <span class="token punctuation">}</span><span class="token punctuation">)</span></code></pre> <p>Happy testing!</p> Good links: 12 May 2024 2024-05-12T00:00:00Z https://localghost.dev/blog/good-links-2024-05-12/ <ul> <li><a href="https://www.takahe.org.nz/heat-death-of-the-internet/?utm_medium=feed&amp;utm_source=feedpress.me&amp;utm_campaign=Feed%3A+coryd-links">Heat Death of the Internet - takahē</a> - “The first page of Google results are links to pages that have scraped other pages for information from other pages that have been scraped for information. All the sources seem to link back to one another. There is no origin. The photos on the page look weird. The hands are disfigured. There is no image credit.”</li> <li><a href="https://simonwillison.net/2024/May/8/slop/">Slop is the new name for unwanted AI-generated content</a> - I love this term, it perfectly sums up what garbage is being created with AI.</li> </ul> New keyboard alert! Mykeyclub MKC75 2024-05-05T00:00:00Z https://localghost.dev/blog/new-keyboard-alert-mykeyclub-mkc75/ <p>Say hello to the <a href="https://www.mykeyclub.com/pages/mkc75-preorder-page">Mykeyclub MKC75</a>!</p> <figure><picture><source type="image/webp" srcset="https://localghost.dev/img/YCOZhibkjT-280.webp 280w, https://localghost.dev/img/YCOZhibkjT-640.webp 640w, https://localghost.dev/img/YCOZhibkjT-960.webp 960w" sizes="auto"><img loading="lazy" decoding="async" src="https://localghost.dev/img/YCOZhibkjT-280.jpeg" alt="The keyboard on a white desk. It has a pastel purple case, and white keys with pastel coloured accents: a purple CMD key, a blue alt key, and a green ISO enter key. The F-key row are all pastel coloured fruits. On the top right is an iridescent purple knob." width="960" height="720" srcset="https://localghost.dev/img/YCOZhibkjT-280.jpeg 280w, https://localghost.dev/img/YCOZhibkjT-640.jpeg 640w, https://localghost.dev/img/YCOZhibkjT-960.jpeg 960w" sizes="auto"></picture></figure> <p>It's so pretty! This is going to be my new office keyboard. Weighted aluminium case, so nice and heavy without too much rattle. It's got a rotary encoder (that's a knob to you and me) which controls the system volume (I look forward to forgetting it's there).</p> <figure><picture><source type="image/webp" srcset="https://localghost.dev/img/1boVuFWaIT-280.webp 280w, https://localghost.dev/img/1boVuFWaIT-640.webp 640w, https://localghost.dev/img/1boVuFWaIT-960.webp 960w" sizes="auto"><img loading="lazy" decoding="async" src="https://localghost.dev/img/1boVuFWaIT-280.jpeg" alt="The keyboard with no keycaps on, showing the switches underneath. They have pastel purple housing with pastel pink, yellow and blue stems" width="960" height="720" srcset="https://localghost.dev/img/1boVuFWaIT-280.jpeg 280w, https://localghost.dev/img/1boVuFWaIT-640.jpeg 640w, https://localghost.dev/img/1boVuFWaIT-960.jpeg 960w" sizes="auto"></picture></figure> <p>I put in <a href="https://dangkeebs.com/products/haluhalo-v2">Dangkeebs HaluHalo linear switches</a> which I got from <a href="https://www.serpentkeys.co.uk/">SerpentKeys</a>. They're a tiny bit louder than reds, but not much. I've recently discovered how dreamy lubricated switches are, although the <a href="https://switchandclick.com/the-ultimate-guide-lube-your-mechanical-keyboard-switches/">process of lubing switches</a> takes fucking forever – it took me at least 2 episodes of Fallout and several episodes of Kin – but typing on them is just delightful.</p> Twitter reply guys were bad, but Mastodon is no better 2024-05-04T00:00:00Z https://localghost.dev/blog/twitter-reply-guys-were-bad-but-mastodon-is-no-better/ <p>Mastodon has often been touted as some kind of grassroots utopia compared to the bro-niverse that is Twitter. But in my experience it's exactly the same: full of sanctimonious reply guys.</p> <p>Before the great Twitter exodus I'd amassed quite a following through a carelessly curated campaign of shitposting, and occasionally a post would go viral. Some really quite nasty things happened sometimes.</p> <p>Storytime: back in my purple hair days, I posted a picture of my hair and makeup colour-matching my IDE, next to a screenshot of some janky wireframe HTML I'd written to throw a webpage screenshot together for a talk. You may have seen it reposted on some content farm.</p> <p>Most of the replies were harmless and many were nice, but a few people &quot;helpfully&quot; commented that the code wasn't accessible. I was at the pub by this point, a few beers in, and posted an annoyed response that people should stop commenting on the code because that wasn't the point. (It absolutely was inaccessible; but it absolutely didn't matter because it was just garbage code for the purposes of a screenshot.)</p> <p><picture><source type="image/webp" srcset="https://localghost.dev/img/jh2tGBLvlg-280.webp 280w, https://localghost.dev/img/jh2tGBLvlg-640.webp 640w" sizes="auto"><img loading="lazy" decoding="async" src="https://localghost.dev/img/jh2tGBLvlg-280.jpeg" alt="A tweet from @type__error that says 'Rule #1: Always colour match your IDE', with two pictures side by side: me with purple hair and eye makeup, and some HTML in an IDE with purple background and pastel coloured code." width="640" height="425" srcset="https://localghost.dev/img/jh2tGBLvlg-280.jpeg 280w, https://localghost.dev/img/jh2tGBLvlg-640.jpeg 640w" sizes="auto"></picture><br> (Side note, I miss that hair, but I don't miss the upkeep or the damage.)</p> <p>Then a well-known tech dude who should have known better took a <em>screenshot</em> of my tweet and reply (not just a RT!) and posted it on his Twitter. I got bombarded with replies: one dude said &quot;but accessibility is important&quot;. Correct, it is, but that wasn't the point of the post. I replied: &quot;You know what else is important? Not being that guy who jumps into a woman's tweet replies to correct her code when she's not even posting about the code and has not asked for any feedback on said code&quot;. Immediately the replies came in: WHY IS GENDER ANYTHING TO DO WITH IT WAH. Some guy <em>tweeted at my employer</em> to tell them that I was being sexist (I had to message our head of social to ask him politely to disregard that).</p> <p>All the shitty replies I got were from men though. Just saying.</p> <p>I like men very much. I work with many men (as well as a good amount of women, considering the industry average) and they are lovely indeed. I am married to a wonderful man. But in my experience, I do not get anywhere <em>near</em> this level of shit on the internet from women, or at least users that are visibly female/non-binary.</p> <p>Perhaps these men reply like this to everyone on the internet, regardless of the OP's gender. Being an indiscriminate knobhead doesn't change the fact that you're being a knobhead.</p> <p>The screenshotted post surfaced on Reddit and even 4chan (shudder) a few times over the next few <em>years</em> causing me to have to lock my Twitter account temporarily each time. I know I could have avoided this entire debacle by not engaging a few beers in, and just muting or locking the thread, but also people didn't need to be dicks about it.</p> <p>I locked my Twitter and abandoned ship in November 2022 when it was looking like it was sinking. A lot of folks are still on there, but it seems like even more of a hellhole. So, like many folks, I migrated to Mastodon. It's... fine? I do actually miss the algorithm, but I follow some cool web folks and I like to see what people are up to. I don't engage nearly as much as I used to with Twitter, which can only be a good thing, really. My relationship to social media has changed a lot.</p> <p>But every now and then, something I post will go... Mastodon viral, I guess. And there's this special kind of reply guy on Mastodon who I hadn't seen before. The person who will take some kind of personal offence to your post because they <em>do not personally relate to it</em>. I posted a link to the <a href="https://www.mcsweeneys.net/articles/the-millennial-captcha">Millennial CAPTCHA</a> because it's amazing, and the sheer number of replies I got explaining that it was Bad, Actually for reasons including:</p> <ul> <li>nobody knows who Michael Cera is (they absolutely do, he is very famous, this is a you problem)</li> <li>nobody knows who Sufjan Stevens is (you don't need to know to complete the test)</li> <li>it's impossible if you're European (I'm English – we may have left the EU but last time I checked we were still in Europe)</li> <li>generational politics are stupid</li> <li>Gen X can do these things too, but nobody ever talks about Gen X!</li> </ul> <p>Honestly, it makes me want to not post things. My Mastodon app (Ice Cubes) hasn't implemented thread muting yet, and I really wish it would hurry up (I have to do it via the browser and hope that the app picks up the setting). I also wish there was a kind of &quot;circles&quot; feature where you could post things that are only visible to a defined list; I know you can post to followers only, but I don't want to have to restrict my followers.</p> <p>This certainly isn't the first time I've had frustrations like this and it won't be the last. I posted my feelings about AI song generators (they make me sad) and got a few replies explaining – you guessed it – why I was wrong, actually. One guy said it's just like how photography came along when before there was only painting. The mind boggles.</p> <p>Just imagine if you could see a post and think “hm, I don’t really get this, I guess it’s not for me!” and then go back to whatever you were doing. Imagine!</p> All Day Hey 2024, and some stuff I bought in Leeds 2024-05-04T00:00:00Z https://localghost.dev/blog/all-day-hey-2024-and-some-stuff-i-bought-in-leeds/ <p>First conference of the year is done, and what a conference it was! I begged Josh to let me speak at <a href="https://heypresents.com/conferences/2024">All Day Hey</a> in 2022, and he promised me a slot in 2024 which at the time seemed forever away. As seems to happen a lot now, I blinked and it was just around the corner.</p> <p>It was the first outing for my new talk on technical migrations, and I'm really pleased with how it went. If you missed All Day Hey, you'll be able to catch it at <a href="https://pixelpioneers.co/">Pixel Pioneers</a> in June, and <a href="https://smashingconf.com/freiburg-2024/">Smashing Freiburg</a> in September.</p> <p>I'm not going to write a whole recap of the conference because I am absolutely thoroughly exhausted, but it was obviously brilliant and you should definitely go next year. Top tier lovely vibes.</p> <p>Yesterday morning before my train home I had a mooch around the Corn Exchange with <a href="https://carol.gg">Carol</a>. We popped in to the lovely bookshop <a href="https://coloursmayvary.com">Colours May Vary</a> where I picked up a copy of <a href="https://coloursmayvary.com/products/gardeners-folklore-the-ancient-secrets-for-gardening-magic-margaret-baker">Gardener's Folklore</a>, originally published in 1976. I'm looking forward to leafing through that and perhaps even picking out some useful tips among the absolutely batshit suggestions and stories.</p> <figure><picture><source type="image/webp" srcset="https://localghost.dev/img/X-fr1zjwub-280.webp 280w, https://localghost.dev/img/X-fr1zjwub-640.webp 640w, https://localghost.dev/img/X-fr1zjwub-960.webp 960w" sizes="auto"><img loading="lazy" decoding="async" src="https://localghost.dev/img/X-fr1zjwub-280.jpeg" alt="A collection of new purchases on a desk: a coaster featuring an opossum that says 'Due to personal reasons aaaaaaaaaaaaaaa'; an iron-on patch featuring Lisa Simpson pointing and saying 'The whole damn system is wrong!'; a pin badge of Gudetama being lazy and lying down; a heart-shaped pin badge featuring Skeletor that says 'Live Laugh Love'; a pin badge with a toadstool, a frog, and phases of the moon that says 'see the magic'; and a square print of a caterpillar wearing a beret in front of a computer with the caption 'I do not care, I will write whatever I want and post it online'." width="960" height="1280" srcset="https://localghost.dev/img/X-fr1zjwub-280.jpeg 280w, https://localghost.dev/img/X-fr1zjwub-640.jpeg 640w, https://localghost.dev/img/X-fr1zjwub-960.jpeg 960w" sizes="auto"></picture></figure> <p>We got lured in to <a href="https://www.giant-kitten.com/">Giant Kitten</a> by a glass cabinet full of pin badges, and I picked up a few excellent ones to add to my collection along with a great patch and a coaster. My favourite is the Skeletor one, I'm obsessed. I also got this really lovely print by <a href="https://claricetudor.com">Clarice Tudor</a> which for me completely and utterly sums up the beauty of personal websites.</p> <p>Highly relatable coaster by <a href="https://www.etsy.com/shop/Betiobca">Betiobca</a>; magic pin by <a href="https://wildflower.co">Wildflower</a> and is not <em>that</em> kind of magic; my soulmate Gudetama pin from <a href="https://punkypins.co.uk">PunkyPins</a>. Skeletor pin and Lisa patch seem to have lots of versions on the internet.</p> Good links: 28 April 2024 2024-04-28T00:00:00Z https://localghost.dev/blog/good-links-2024-04-28/ <ul> <li><a href="https://rachelandrew.co.uk/archives/2024/04/21/on-having-no-visual-memory/">On having no visual memory</a> - Rachel Andrew shares her experiences of what it’s like to have aphantasia.</li> <li><a href="https://meryl.net/why-captioned-videos-are-important/">Why Good Captioned Videos Are Important – Meryl.net home</a> - A really insightful guide to what makes good, accessible video captions. Plain is always better, folks!</li> </ul> Good links: 14 April 2024 2024-04-14T00:00:00Z https://localghost.dev/blog/good-links-2024-04-14/ <ul> <li><a href="https://bjhess.com/posts/you-re-a-blogger-not-an-essayist">You’re a Blogger, Not an Essayist - I am BARRY HESS</a> - “You don’t need to labor over your posts. You don’t need to have perfect grammar or spelling. You don’t need to leave a post in draft for seven months, pouring over research. (Though you can if you want!) You don’t really need to have an idea.</li> </ul> <p>Just write. Then share.”</p> <ul> <li><a href="https://heydonworks.com/article/testing-html-with-modern-css/">Testing HTML With Modern CSS: HeydonWorks</a> - Heydon’s useful CSS bag of tricks for diagnosing poor HTML semantics.</li> <li><a href="https://coryd.dev/posts/2024/a-retrospective-on-a-year-without-streaming-music/?utm_medium=feed&amp;utm_source=feedpress.me&amp;utm_campaign=Feed%3A+coryd">A retrospective on a year without streaming music // Cory Dransfeldt</a> - Streaming is the norm now - I certainly do it, via Apple Music - but some people are going back to buying mp3s and consuming music that way. It’s certainly better for the artists.</li> </ul> A good recipe: Ways with rhubarb 2024-04-09T00:00:00Z https://localghost.dev/blog/a-good-recipe-ways-with-rhubarb/ <p>It's rhubarb season! I came home from my parents' last weekend with a large armful of the stuff.</p> <p>Rhubarb is extremely tart and not very nice on its own unless you drown it in sugar. It's particularly good when paired with very sweet things to balance out the sharp edge of the fruit, hence its classic pairing with custard. I love the pleasingly pink hue of it when it's cooked.</p> <figure><picture><source type="image/webp" srcset="https://localghost.dev/img/jyuD6baaVx-280.webp 280w, https://localghost.dev/img/jyuD6baaVx-640.webp 640w, https://localghost.dev/img/jyuD6baaVx-960.webp 960w" sizes="auto"><img loading="lazy" decoding="async" src="https://localghost.dev/img/jyuD6baaVx-280.jpeg" alt="A tin of rhubarb and custard blondies, golden coloured squares studded with white chocolate and slices of rhubarb." class="small" width="960" height="1280" srcset="https://localghost.dev/img/jyuD6baaVx-280.jpeg 280w, https://localghost.dev/img/jyuD6baaVx-640.jpeg 640w, https://localghost.dev/img/jyuD6baaVx-960.jpeg 960w" sizes="auto"></picture></figure> <p>On Sunday night I baked a batch of Delicious magazine's <a href="https://www.deliciousmagazine.co.uk/recipes/rhubarb-and-custard-blondies/">rhubarb and custard blondies</a> and WOW they are delicious indeed. The &quot;custard&quot; is actually brown butter with white chocolate and vanilla extract stirred into it, and I think the base recipe would be a fabulously versatile blondie that you could chuck all sorts of fruits into. The tart rhubarb cuts into the sweetness beautifully.</p> <p>I flash-froze a couple more batches of sliced rhubarb (blanch by immersing in boiling water for 1 min, then plunge into ice cold water), and I'm going to stew the rest using this lovely <a href="https://www.jamieoliver.com/recipes/fruit-recipes/stewed-rhubarb-and-vanilla-yoghurt/">Jamie Oliver recipe</a>. Last year I used some of this stewed rhubarb in an eton mess.</p> <p>Most of the rhubarb recipes I've got are sweet, and there's only so many cakes one can make (though I'm sure my husband would disagree). I'm on the hunt for some more savoury recipes. In the past I've tried this recipe for <a href="https://www.theguardian.com/food/2022/mar/05/lamb-pilaf-chard-goats-cheese-galette-rhubarb-recipes-ravinder-bhogal">lamb bulgur pilaf with rhubarb and almonds</a>, which was pretty tasty but I don't buy lamb very often. This fruit is a good counterpart to fatty meats, and I wonder if it'd pair well with fish in the same way that citrus does. <a href="https://www.theguardian.com/lifeandstyle/2010/feb/07/nigel-slater-rhubarb-mackerel-recipes">Old mate Nige</a> seems to think so, choosing a fattier fish (mackerel).</p> Upcoming talk: All Day Hey, Leeds - 2nd May 2024 2024-04-01T00:00:00Z https://localghost.dev/blog/upcoming-talk-all-day-hey-leeds-2nd-may-2024/ <p>Two years after <a href="https://heypresents.com/conferences/2024">All Day Hey</a> made me <a href="https://localghost.dev/blog/when-going-back-doesn-t-mean-going-backwards/">realise I missed the web</a>, I'm really excited to be speaking there myself! I'll be talking about <strong>technical migrations</strong> – how we recently pulled off a Typescript migration at Monzo, and things you should consider when doing a migration yourself.</p> <p>And what a line up - my good pals <a href="https://hidde.blog">Hidde</a> and <a href="https://ohhelloana.blog">Ana</a>, plus UX whiz <a href="https://www.imranafzal.com/">Imran Afzal</a>, and two of my lovely Monzo colleagues Heldiney and Giorgio. It's a complete coincidence we ended up speaking at the same conference! I've spoken alongside Heldiney once before, at UX London 2022 – he was just brilliant.</p> <p>All Day Hey is certified good vibes throughout, PLUS it's in a cinema that has sofas instead of cinema seats. SOFAS. It's not long to go now so make sure you've got your ticket!</p> <p><a href="https://heypresents.com/conferences/2024">All Day Hey 2024</a></p> <figure><picture><source type="image/webp" srcset="https://localghost.dev/img/8lpjxQpAMS-280.webp 280w, https://localghost.dev/img/8lpjxQpAMS-640.webp 640w, https://localghost.dev/img/8lpjxQpAMS-960.webp 960w" sizes="auto"><img loading="lazy" decoding="async" src="https://localghost.dev/img/8lpjxQpAMS-280.jpeg" alt="The poster for All Day Hey. 09:00-17:00 BST, Thursday 2nd May 2024, Everyman Cinema, Leeds. Sophie Koonin (Monzo), Hidde de Vries, Phil Hawksworth (Netlify), Heldiney Pereira &amp; Giorgio Baglioni (Monzo), Ana Rodrigues (Hactar), Imran Afzal (Co-op). All Day Hey! 2024. heypresents.com/conference" width="960" height="960" srcset="https://localghost.dev/img/8lpjxQpAMS-280.jpeg 280w, https://localghost.dev/img/8lpjxQpAMS-640.jpeg 640w, https://localghost.dev/img/8lpjxQpAMS-960.jpeg 960w" sizes="auto"></picture></figure> Good links: 31 March 2024 2024-03-31T00:00:00Z https://localghost.dev/blog/good-links-2024-03-31/ <ul> <li><a href="https://downpour.games/~terry/a-proper-cup-of-tea">&quot;a proper cup of tea&quot; by Terry</a> - An absolutely joyously chaotic tea-making game. I recommend trying all the different combinations for maximum mirth.</li> </ul> Good links: 17 March 2024 2024-03-17T00:00:00Z https://localghost.dev/blog/good-links-2024-03-17/ <ul> <li><a href="https://theinternet.review/2024/03/13/ready-to-party-like-its-1999/">Why I’m Ready to Party Like It’s 1999…Again</a> - Jared White reflects on his early experiences of the web, and suggests maybe we’re ready to do the 90s web again…</li> </ul> Some things I've been enjoying recently 2024-03-16T00:00:00Z https://localghost.dev/blog/some-things-i-ve-been-enjoying-recently/ <p>This isn't going to be a tech post, mainly because I haven't really been enjoying tech things recently. I spend all day thinking about technical integrations and system architectures and requirements, so I don't really want to do anything dev-related when I get home. Every time I see people talking about Astro or web components or new CSS features I feel bad because I know I should try it out and see what all the fuss is about but I really can't be arsed soz.</p> <p>I'm procrastinating talk-writing so naturally I thought I'd write a blog post. Here are some shows, books, theatre etc I've enjoyed recently.</p> <h2 id="tv" tabindex="-1">TV</h2> <p>Apple TV continues to bring out excellent series. We've been making our way through <a href="https://en.wikipedia.org/wiki/For_All_Mankind_(TV_series)">For All Mankind</a>, an alternate history sci-fi series about what could've transpired if the USSR had won the space race in the late 60s. Each series takes place a decade after the last, which makes for some interesting aging techniques on the actors. It's truly excellent – I think I can safely say there hasn't been a single dud episode in four seasons. It's produced by Ronald D. Moore of Battlestar Galactica fame.</p> <p>Also on Apple TV is <a href="https://www.youtube.com/watch?v=kq5TmH7Np1M">The Completely Made-Up Adventures of Dick Turpin</a> starring Noel Fielding as, well, a coach-robbing <a href="https://mightyboosh.fandom.com/wiki/Vince_Noir">Vince Noir</a>. It's utterly silly and very funny in a Horrible Histories/<a href="https://en.wikipedia.org/wiki/Ghosts_(2019_TV_series)">Ghosts</a>/Blackadder kind of way, and has lots of familiar faces popping up throughout.</p> <p>And on Apple TV again... another space-themed drama called <a href="https://en.wikipedia.org/wiki/Constellation_(TV_series)">Constellation</a>, where an accident on the ISS involving an astronaut and a NASA science experiment leads to some weird parallel universe goings-on. I love a good parallel universe story (Fringe was a favourite of mine) and I bingewatched the first six episodes while I was off work sick. It's the kind of series where you'll go on Reddit afterwards to read up on other people's theories.</p> <p>I like watching things while I cook, and the perfect cookery series of recent times has been <a href="https://www.imdb.com/title/tt2191671/">Elementary</a>. Classic procedural with crime-solving duo premise, but with a bit of light humour and some great performances. I'm delighting in seeing all the early 2010s mobile phones, and it's one of those shows where as soon as a vaguely familiar actor pops up in a bit part, you know they're probably the killer.</p> <h2 id="theatre" tabindex="-1">Theatre</h2> <p>We went to see <a href="https://www.operationmincemeat.com/">Operation Mincemeat</a> on some friends' recommendation, with little idea what to expect. It's a musical based on the <a href="https://en.wikipedia.org/wiki/Operation_Mincemeat">WWII operation</a> where MI5 planted some fake intel on a body that &quot;washed up&quot; on the beach in Spain, misleading the Nazis and allowing the Allies to invade Sicily. It's very very funny, though the theatre is extremely tiny and cramped, so tall people beware. My husband was folded up like a concertina for most of it.</p> <p>We also caught an NT Live screening of <a href="https://www.nationaltheatre.org.uk/whats-on/vanya/">Vanya</a>, a one-man adaptation of Chekhov's Uncle Vanya. Andrew Scott is in it, and he's absolutely brilliant in everything he does – I also saw All Of Us Strangers recently, and he was fantastic in that too. Vanya is still on in a few cinemas, I think.</p> <h2 id="art" tabindex="-1">Art</h2> <p>I met my parents at the Tate Britain the other weekend. I hadn't been for many years, and I think last time I was there I was too young to actually appreciate any of the art. I hadn't realised Millais' <a href="https://www.tate.org.uk/art/artworks/millais-ophelia-n01506/story-ophelia">Ophelia</a> was in there: it's a beautiful painting but also the basis of one of my favourite memes. I appreciate both for the art forms they are.</p> <figure><picture><source type="image/webp" srcset="https://localghost.dev/img/Q8f9i0fL7S-280.webp 280w, https://localghost.dev/img/Q8f9i0fL7S-640.webp 640w, https://localghost.dev/img/Q8f9i0fL7S-960.webp 960w" sizes="auto"><img loading="lazy" decoding="async" src="https://localghost.dev/img/Q8f9i0fL7S-280.jpeg" alt="Millais' painting of Ophelia, with a pale curly-haired woman in an embroidered dress lying on her back in the water surrounded by lush verdant greenery and flowers. Superimposed on top is a tweet by @daisandconfused that says &quot;The girl boss is dead, long live the girl moss (lying on her back and being absorbed back into nature)&quot;" width="960" height="958" srcset="https://localghost.dev/img/Q8f9i0fL7S-280.jpeg 280w, https://localghost.dev/img/Q8f9i0fL7S-640.jpeg 640w, https://localghost.dev/img/Q8f9i0fL7S-960.jpeg 960w" sizes="auto"></picture></figure> <p>The Turners are stunning, the <a href="https://www.tate.org.uk/visit/tate-britain/display/jmw-turner/the-sea-toil-and-terror">rough seascapes</a> especially, but I also really loved his <a href="https://www.tate.org.uk/visit/tate-britain/display/jmw-turner/travels-in-europe">paintings of Europe</a>. He had a way of really capturing the light.</p> <p>Later I popped into <a href="https://www.tate.org.uk/whats-on/tate-britain/women-in-revolt">Women in Revolt!</a>, the Tate's exhibition of feminist art and activism from 1970-1990. A real diverse range of artworks, protests, political pieces and campaigns: including equal pay &amp; division of labour, perceptions of lesbianism, the DIY scene, protesting nuclear weaponry, Black feminist art, AIDS, and the Thatcher government. It's on until 7th April.</p> <div class="content-grid"> <figure> <a href="https://localghost.dev/img/blog/things-ive-been-enjoying/anne-bean.jpeg"> <picture><source type="image/webp" srcset="https://localghost.dev/img/CZfwgMin3a-280.webp 280w, https://localghost.dev/img/CZfwgMin3a-640.webp 640w, https://localghost.dev/img/CZfwgMin3a-960.webp 960w" sizes="auto"><img loading="lazy" decoding="async" src="https://localghost.dev/img/CZfwgMin3a-280.jpeg" alt="An artwork hanging on the wall at the Tate Britain. A series of 9 framed photographs in a grid showing the artist's face appearing to be on fire" width="960" height="1279" srcset="https://localghost.dev/img/CZfwgMin3a-280.jpeg 280w, https://localghost.dev/img/CZfwgMin3a-640.jpeg 640w, https://localghost.dev/img/CZfwgMin3a-960.jpeg 960w" sizes="auto"></picture> </a> <figcaption><i>Heat</i> by Anne Bean</figcaption> </figure> <figure> <a href="https://localghost.dev/img/blog/things-ive-been-enjoying/thatcher.jpeg"> <picture><source type="image/webp" srcset="https://localghost.dev/img/pJqvnnp-Kc-280.webp 280w, https://localghost.dev/img/pJqvnnp-Kc-640.webp 640w, https://localghost.dev/img/pJqvnnp-Kc-960.webp 960w" sizes="auto"><img loading="lazy" decoding="async" src="https://localghost.dev/img/pJqvnnp-Kc-280.jpeg" alt="A 1980s artwork showing Margaret Thatcher with the words 'my message to the women of our nation' and a speech bubble coming from Thatcher's face saying 'TOUGH!'. The image of Thatcher is surrounded by illustrations describing ways in which her government has harmed women: hospitals closed, nurseries closed, council houses for sale, unemployment, £4,000,000,000 cut in public spending by 1981, elderly care centres closed, £308,000,000 cut from housing budget, fares increase, VAT up to 15%, £55,000,000 cut from education programme, gas and electricity up" width="960" height="1279" srcset="https://localghost.dev/img/pJqvnnp-Kc-280.jpeg 280w, https://localghost.dev/img/pJqvnnp-Kc-640.jpeg 640w, https://localghost.dev/img/pJqvnnp-Kc-960.jpeg 960w" sizes="auto"></picture> </a> <figcaption>Tough (1979) - The See Red Women's Workshop</figcaption> </figure> </div> <h2 id="books" tabindex="-1">Books</h2> <p>I've made it about 75% of the way through <a href="https://en.wikipedia.org/wiki/The_Three-Body_Problem_(novel)">The Three Body Problem</a>, a sci-fi novel about Chinese astrophysicists discovering an alien civilisation... well, I don't want to give it away, and it's really good, but slow reading. I'm a speed reader, but I found I needed to really focus on the book and I tend to read before bed, so I make it about three pages in and have to put the light out. (I should read more on my commute.)</p> <p>I read A Court of Thorns and Roses because it seems to be everywhere and I thought it'd be a good break from books that made me think about things. But it's actually... really good? I just finished the third book in the series. Playing Baldur's Gate 3 got me into fantasy (I'd only ever read Terry Pratchett, tried reading some Robin Hobb and just couldn't get into it but I'll give it another go sometime) and this is &quot;romantasy&quot; which is a whole genre that'd completely escaped my awareness until recently. But I'm now extremely invested in the storyline and I heard that old mate Ronald D. Moore has plans to produce a series based on it, so if he's involved you know it's good.</p> <h2 id="food" tabindex="-1">Food</h2> <p>I made Smitten Kitchen's <a href="https://smittenkitchen.com/2024/03/weeknight-tomato-soup/">weeknight tomato soup</a>, and it was heavenly. The splash of sherry really lifts it. I made it with the grilled cheese &quot;fingers&quot; as Deb Perelman suggested, and it was a delight.</p> <p>Deb really can't fail – I made her <a href="https://smittenkitchen.com/2023/12/brown-butter-brown-sugar-shortbread/">brown sugar brown butter shortbread</a> recently and brought them to work, and they disappeared in record time. I think it might be the most popular thing I've ever made. Brown butter is having a moment, lads.</p> <p>One of my favourite lunches near the office is at Spitalfields Market, a pricey covered market near Liverpool Street that's full of street food stalls, with some absolute gems. My favourites include soup dumplings from <a href="https://oldspitalfieldsmarket.com/food-and-drink/dumpling-shack">Dumpling Shack</a>, vegan Ethiopian mixed veg and grains with injera from <a href="https://oldspitalfieldsmarket.com/food-and-drink/merkamo-ethiopia">Merkamo</a>, and the oft-overlooked Sri Lankan kothu roti place, <a href="https://oldspitalfieldsmarket.com/food-and-drink/karapincha">Karapincha</a>. The other day I decided I wanted to try making kothu roti myself. I found this <a href="https://sortedfood.com/2021/07/29/karan-hoppers-chicken-kothu-roti-recipe/">recipe</a> from Hoppers (where I <em>still</em> haven't been!) and it was delicious, though after making it once I think I'd do the curry bit the day before, as it took a while to do the whole thing in one go, even using premade rotis. Hard recommend, though.</p> <p>Finally, I treated myself to lunch at <a href="https://fallowrestaurant.com/">Fallow</a> on Haymarket recently, managing to book a seat at the chef's table where you can watch the chefs working. In a bizarre and wonderful twist, a couple was seated next to me and it turned out to be my friend/tech lead from my first proper dev role (one of my favourite teams ever). We had a good catch up, and she let me try some of her, er, <a href="https://www.greatbritishchefs.com/recipes/cods-head-with-sriracha-butter-sauce-recipe">cod's head</a>. A couple of must-try dishes are the mushroom parfait which made me see stars, and the corn ribs which are very, very, <em>very</em> good. They have a set lunch menu which includes the mushroom parfait. For dessert, I had a rhubarb and ginger pavlova with jasmine tea, and it was so potent I felt like I was sitting next to an actual jasmine bush. Absolutely delightful.</p> <div class="content-grid"> <figure> <picture><source type="image/webp" srcset="https://localghost.dev/img/zAIIpYD3dc-280.webp 280w, https://localghost.dev/img/zAIIpYD3dc-640.webp 640w, https://localghost.dev/img/zAIIpYD3dc-960.webp 960w" sizes="auto"><img loading="lazy" decoding="async" src="https://localghost.dev/img/zAIIpYD3dc-280.jpeg" alt="Golden slices of toast are stacked on a wooden board next to a scoop of caramel-coloured mushroom parfait. The parfait has shavings of white mushroom on top. In the background, out of focus, is a bowl of dark brown crispy corn ribs, served with a wedge of lime." width="960" height="1279" srcset="https://localghost.dev/img/zAIIpYD3dc-280.jpeg 280w, https://localghost.dev/img/zAIIpYD3dc-640.jpeg 640w, https://localghost.dev/img/zAIIpYD3dc-960.jpeg 960w" sizes="auto"></picture> <figcaption>Fallow's mushroom parfait, with corn ribs in the background</figcaption> </figure> <figure> <picture><source type="image/webp" srcset="https://localghost.dev/img/xIiHWAK3C7-280.webp 280w, https://localghost.dev/img/xIiHWAK3C7-640.webp 640w, https://localghost.dev/img/xIiHWAK3C7-960.webp 960w" sizes="auto"><img loading="lazy" decoding="async" src="https://localghost.dev/img/xIiHWAK3C7-280.jpeg" alt="A scoop of rhubarb sorbet sits on top of a perfectly domed meringue, surrounded by a bright pink moat of rhubarb and jasmine sauce" width="960" height="1279" srcset="https://localghost.dev/img/xIiHWAK3C7-280.jpeg 280w, https://localghost.dev/img/xIiHWAK3C7-640.jpeg 640w, https://localghost.dev/img/xIiHWAK3C7-960.jpeg 960w" sizes="auto"></picture> <figcaption>Rhubarb pavlova with stem ginger and jasmine tea</figcaption> </figure> </div> Good links: 10 March 2024 2024-03-10T00:00:00Z https://localghost.dev/blog/good-links-2024-03-10/ <ul> <li><a href="https://piccalil.li/blog/some-little-ways-im-using-css-has-in-the-real-world/">Some little ways I’m using CSS :has() in the real world - Piccalilli</a> - Some great tips for one of my favourite recent CSS features.</li> <li><a href="https://lynnandtonic.com/thoughts/entries/case-study-2023-refresh/">Case Study: lynnandtonic.com 2023 refresh</a> - Lynn’s annual redesigns are a real joy, and this is a fascinating look into the inspiration and technique that went into the 2023 edition.</li> <li><a href="https://notes.neatnik.net/2024/03/motive-as-a-filter">Neatnik Notes · Motive as a filter</a> - When you use a free email service like Gmail, what’s Google’s motive for offering it?</li> </ul> Good links: 3 March 2024 2024-03-03T00:00:00Z https://localghost.dev/blog/good-links-2024-03-03/ <ul> <li><a href="https://nuejs.org/blog/tailwind-vs-semantic-css/">Tailwind vs Semantic CSS</a> - Found via Andy Bell's site - as he puts it, this post is pretty clearly biased towards semantic CSS <em>but</em> I think it's pretty evident fromt his that bare-bones HTML with &quot;plain old CSS&quot; is a lot more performant than stuffing a page full of unnecessary divs and stacking utility classes.</li> <li><a href="https://chriscoyier.net/2024/02/28/where-im-at-on-the-whole-css-tricks-thing/">Where I’m at on the whole CSS-Tricks thing</a> - It was March 2022 when I sold CSS-Tricks to DigitalOcean. So it’s been just about 2 years now. This was me and my wife’s thinking: The negotiated sale price was fair. They are a big com…</li> <li><a href="https://coryd.dev/posts/2024/towards-a-quieter-friendlier-web/">Towards a quieter, friendlier web • Cory Dransfeldt</a> - &quot;You aren't obligated to reply or participate in any discussion. Don't feel bad about not engaging if it doesn't serve you. Arguing on the internet is rarely healthy, dialogue and discussion certainly can be.&quot;</li> <li><a href="https://adrianroselli.com/2024/02/techniques-to-break-words.html">Techniques to Break Words</a> - A good collection of useful techniques to bookmark here!</li> <li><a href="https://rss-is-dead.lol/">RsS iS dEaD LOL</a> - My colleague Paul made this brilliant little site for finding RSS feeds of people you follow on mastodon (and people they follow, etc)</li> </ul> Good links: 25 February 2024 2024-02-25T00:00:00Z https://localghost.dev/blog/good-links-2024-02-25/ <ul> <li><a href="https://robbowen.digital/wrote-about/abandoned-side-projects/">It's OK to abandon your side-project - Robb Owen</a> - &quot;We hear about all the side-project success stories, but what if we talked more openly about the ones that tanked?&quot;</li> <li><a href="https://robinrendle.com/notes/mini-manifesto/">Robin Rendle — Mini Manifesto</a> - Robin has redone his homepage and it’s utterly glorious.</li> </ul> Good links: 18 February 2024 2024-02-18T00:00:00Z https://localghost.dev/blog/good-links-2024-02-18/ <ul> <li><a href="https://midnight.pub/">The Midnight Pub</a> - A tiny forum in the form of a virtual pub. It even has a speakeasy.</li> <li><a href="https://frills.dev/blog/070224-this-website-is-personal-girls/">This website is personal - Frills</a> - I really identify with this post, and the feeling that my posts aren’t worth anything if they’re not useful in some way. In reality there are no rules at all about what you should post on a personal site!</li> <li><a href="https://adrianroselli.com/2024/02/dont-disable-form-controls.html">Don’t Disable Form Controls</a> - Stop disabling submit buttons.</li> <li><a href="https://computer.rip/2024-02-11-the-top-of-the-DNS-hierarchy.html">the top of the DNS hierarchy</a> - TLD nameservers are well-known, but what about the very top level - the root? An in-depth look at the thirteen root DNS servers.</li> <li><a href="https://tonsky.me/blog/checkbox/">In Loving Memory of Square Checkbox</a> - How are we supposed to tell the difference now?</li> </ul> Good links: 4 February 2024 2024-02-04T00:00:00Z https://localghost.dev/blog/good-links-2024-02-04/ <ul> <li><a href="https://hamatti.org/posts/please-dont-force-me-to-log-in/">Please, don’t force me to log in</a> - It feels like every website or connected device wants you to sign up before you can use it these days…</li> <li><a href="https://piccalil.li/blog/react-is-getting-a-bit-of-a-kicking-recently/">It feels like React is getting a bit of a kicking recently - Piccalilli</a> - There's a lot of React criticism floating around at the mo, much of it justified. I think people are quick to use it when they don't need to. But as someone working on a very big, very complex project that does benefit from a framework like React, I'm glad Andy is taking a nuanced look at the debate.</li> <li><a href="https://piccalil.li/blog/a-highly-configurable-switch-component-using-modern-css/">A highly configurable switch component using modern CSS techniques - Piccalilli</a> - We’ve all built plenty of switches out of checkboxes, I’m sure, but Andy has updated his technique to use some newer CSS features and produced a rather lovely result.</li> </ul> Listen to this: The Stand-in by Caitlin Rose 2024-01-30T00:00:00Z https://localghost.dev/blog/listen-to-this-the-stand-in-by-caitlin-rose/ <p>At university I worked at Manchester Academy, and would often end up behind the bar at all sorts of gigs from pop, to metal, to folk. Metal gigs were always my favourite because the crowd were very nice to bar staff, whereas sometimes you'd have &quot;POUR OUT CANS&quot; written on the sign in sheet and you knew it was going to be rowdy.</p> <p>Anyway, one of the gigs I ended up working at was Caitlin Rose, a country singer hailing from Tenessee. A tiny gig in our smallest venue, Academy 3, where I could lean back against the bar and watch quite happily. I'd never considered myself a fan of country (though a few years later I'd get very, very emotionally invested in the TV series Nashville, and in fact a song of hers did feature) but I was really taken by her witty and honest lyrics, and catchy refrains.</p> <p>A few years later in 2013 she released this absolute dynamite record, <em>The Stand-In</em>, a vintage-flavoured record full of excellent writing, velvety vocals and heartbreak. If you don't like country, don't be put off: it's more kind of alternative country.</p> <p>She then promptly disappeared for about ten years only to surface with a new album last year (<em>Cazimi</em>). But The Stand-In is still my favourite.</p> <p>Top track: <em>No One To Call</em> is a short one, but a bittersweet plea from a lonely life on the road.</p> Good links: 28 January 2024 2024-01-28T00:00:00Z https://localghost.dev/blog/good-links-2024-01-28/ <ul> <li><a href="https://www.oldavista.com/">Old'aVista</a> - Altavista was my first search engine (or should I say altavista+was+my+first+search+engine) and this is a delightful trip down memory lane.</li> <li><a href="https://jvns.ca/blog/2024/01/26/inside-git/">Inside .git</a> - Legendary explainer of things Julia Evans has written a fascinating and understandable guide to what lives inside the .git folder.</li> <li><a href="https://cari.institute/aesthetics">CARI | Aesthetics</a> - I shared this on Mastodon a while back but this is just so compelling, and some of these aesthetics come with a heady dose of nostalgia.</li> <li><a href="https://www.fromjason.xyz/p/notebook/where-have-all-the-websites-gone/">Where have all the websites gone?</a> - A heartfelt lamentation for the human web.</li> <li><a href="https://ohhelloana.blog/another-round-of-bookmarks/">Oh Hello Ana</a> - Ana Rodrigues has posted a very comprehensive list of good tech and web-related links.</li> </ul> So you've decided to get into mechanical keyboards 2024-01-27T00:00:00Z https://localghost.dev/blog/so-you-ve-decided-to-get-into-mechanical-keyboards/ <p>This is a dangerous path you're about to walk down. Is your wallet ready?</p> <p>Whether you prefer the smooth glide of linear switches, the gentle bump of a tactile switch or the full-on audio assault of a clicky switch, there’s something extremely gratifying about typing on a mechanical keyboard (even if it’s just a reply to someone who is wrong on the internet). And it’s even better if it looks as adorable as this.</p> <figure><picture><source type="image/webp" srcset="https://localghost.dev/img/zUGBThRsrx-280.webp 280w, https://localghost.dev/img/zUGBThRsrx-640.webp 640w, https://localghost.dev/img/zUGBThRsrx-960.webp 960w" sizes="auto"><img loading="lazy" decoding="async" src="https://localghost.dev/img/zUGBThRsrx-280.jpeg" alt="A top-down view of a mechanical keyboard with ISO layout. Its keycaps form a sort of gradient, colours in stripes going diagonally left to right from peach to pink to purple to blue to white. There is also a small purple plate on the keyboard with stars engraved on it. SOme of the keys have cute symbols on like stars, moons, clouds and meteors; the ISO enter key has a constellation on it. The keyboard sits on top of a deskmat with a cat in space on it." width="960" height="686" srcset="https://localghost.dev/img/zUGBThRsrx-280.jpeg 280w, https://localghost.dev/img/zUGBThRsrx-640.jpeg 640w, https://localghost.dev/img/zUGBThRsrx-960.jpeg 960w" sizes="auto"></picture></figure> <p>I’ll level with you: I got into mechanical keyboards because a) they’re so PRETTY! and b) it was the pandemic, what else was I supposed to do? I had been perfectly happy tapping away on my Apple wireless keyboard for a very long time. But I really do prefer them now, and find that I’m much less prone to wrist strain than I was with the extremely flat profile of the Apple keyboard. Sure, I could have bought a Logitech keyboard that’s a bit fatter and be done with it, but it wouldn’t look like this:</p> <figure><a href="https://localghost.dev/img/keyboards/ikki68-2.webp"><picture><source type="image/webp" srcset="https://localghost.dev/img/d90KrwkZ8R-280.webp 280w, https://localghost.dev/img/d90KrwkZ8R-640.webp 640w, https://localghost.dev/img/d90KrwkZ8R-960.webp 960w" sizes="auto"><img loading="lazy" decoding="async" src="https://localghost.dev/img/d90KrwkZ8R-280.jpeg" alt="The ikki68 Aurora keyboard with white and pastel purple polycarbonate case. Most of the keys are off-white, the escape and pg up/pg down/home/end keys are pastel coloured with fruit legends. It is in Mac ISO layout with a split left shift." width="960" height="327" srcset="https://localghost.dev/img/d90KrwkZ8R-280.jpeg 280w, https://localghost.dev/img/d90KrwkZ8R-640.jpeg 640w, https://localghost.dev/img/d90KrwkZ8R-960.jpeg 960w" sizes="auto"></picture></a></figure> <p>I’ll continue to level with you: it is not a cheap hobby. From the keyboard itself, to the switches for each key, to each keyset (the keys themselves, usually sold separately), the cost adds up quickly. The only positive I can offer is that by the time the keyset you ordered arrives, it’s usually been so long you forgot how much it cost you.</p> <p>You’ll also need a lot of <strong>patience</strong>. Since it’s a pretty niche hobby, keyboards and keysets have to go through a few stages before they arrive at your doorstep: garnering interest to see if people want to buy it, presale, production and quality assurance, then delivery and distribution. This can take anywhere between a few months to over a year – in fact, one keyset I ordered back in 2021 has been so beset by production issues and delays that it still hasn’t arrived yet.</p> <h3 id="how-mechanical-keyboards-work" tabindex="-1">How mechanical keyboards work</h3> <p>Your average mechanical keyboard will have:</p> <ul> <li>a case, made out of plastic or aluminium</li> <li>a PCB (printed circuit board) with a USB connector (or Bluetooth if you’re fancy)</li> <li>a controller that can translate those keypresses into input instructions for your computer</li> <li>switches to trigger keypresses</li> <li>keycaps for you to type on</li> <li>stabilisers (aka stabs) for the longer keys so they don’t wobble</li> </ul> <p>Keyboard switches complete a circuit in the keyboard which causes a specific keypress to be transmitted via the controller to the computer.</p> <p>Switches have a plastic shell with a moveable plastic “stem”, the bit that the keycap attaches to and that moves up and down when you press it. There’s a metal spring inside to control that movement, and finally a metal plate with legs that protrude from underneath the switch which you either solder into the keyboard’s PCB, or slot into place on a hot-swap keyboard.</p> <p>Hot-swap means you can remove and change switches without having to do any soldering, which is great if you want to try a few different kinds; but hot-swap PCBs are a bit less common – and more expensive – than regular ones.</p> <h2 id="switches" tabindex="-1">Switches</h2> <p>Mechanical keyboard switches come in three categories: linear (smooth), tactile (bump), and clicky (exactly what it sounds like).</p> <p>All switches have what’s called an “actuation point”, which is the point at which the keypress is registered. It’s usually when you press the switch about halfway down. While linear switches don’t give any feedback that you’ve reached the actuation point, tactile switches will have a bit of resistance or a “bump” that lets you know you’re there, and clicky switches make the click sound at that point. Some people like this because it means you don’t have to push the keys all the way down.</p> <p>It’s completely down to personal preference here: I like linear switches, as I actually prefer my keyboards to be as quiet as possible. If you’re intending to bring your keyboard to a place where other people are working, please don’t buy clicky switches unless you want to find your expensive new keyboard mysteriously covered in tea.</p> <h3 id="mechanical-vs-optical-switches" tabindex="-1">Mechanical vs optical switches</h3> <p>Standard mechanical switches trigger when the stem is pushed down, causing the metal leaf at the bottom of the switch to connect, completing the circuit. Optical switches have springs too, but these have a light signal inside them which registers a keypress. There aren’t quite as many optical switches out there but there isn’t a lot of difference between the two types, it’s just a newer technology.</p> <h3 id="cherry-mx-switches-and-their-colours" tabindex="-1">Cherry MX switches and their colours</h3> <p>You’ll hear people talking about red switches, brown switches, blue switches... this relates to the colour of the stem in Cherry MX switches. These are one of the most popular shapes of switch, and lots of manufacturers make them other than Cherry MX – I tend to buy Gateron as they’re a bit cheaper.</p> <p>The colours tell you what kind of switch it is:</p> <ul> <li>red: linear, light resistance, relatively quiet</li> <li>black: linear, heavy resistance, relatively quiet</li> <li>brown: tactile, moderate sound, medium resistance</li> <li>clear: tactile, moderate sound, heavy resistance</li> <li>blue: loud, clicky, medium resistance</li> <li>green: very loud, clicky, heavy resistance</li> </ul> <figure> <picture><source type="image/webp" srcset="https://localghost.dev/img/NrCuC3qIXv-280.webp 280w, https://localghost.dev/img/NrCuC3qIXv-640.webp 640w, https://localghost.dev/img/NrCuC3qIXv-960.webp 960w" sizes="auto"><img loading="lazy" decoding="async" src="https://localghost.dev/img/NrCuC3qIXv-280.jpeg" alt="Three switches on a table. Two are brown and one is red. One of the brown ones is upside down showing its metal prongs underneath." width="960" height="756" srcset="https://localghost.dev/img/NrCuC3qIXv-280.jpeg 280w, https://localghost.dev/img/NrCuC3qIXv-640.jpeg 640w, https://localghost.dev/img/NrCuC3qIXv-960.jpeg 960w" sizes="auto"></picture> <figcaption>Gateron red and brown switches</figcaption> </figure> <p>There are lots of manufacturers and limited runs of this switch shape with different colours and actions, and it can get quite overwhelming, but you can just stick to the normal colours!</p> <p>If you’re not sure what colour you want a lot of places sell a switch tester with a few different colours in so you can get a feel for what you like most.</p> <p>Which colour you buy is entirely up to you, it’s based on personal preference. What <em>does</em> matter is what shape of switch you buy, and this will usually be specified on the keyboard/PCB listing.</p> <p>There are other types of switches e.g. <a href="https://switchandclick.com/low-profile-switches-explained/">low profile switches</a>, but the majority of keyboards and keycaps that I’ve seen are made for the Cherry MX profile.</p> <h2 id="keyboards" tabindex="-1">Keyboards</h2> <p>The keyboard itself can come in lots of different shapes and sizes. You didn’t think this would be simple, did you?</p> <p>You can buy keyboards as kits ready to be assembled (you'll need some tiny screwdrivers and a soldering iron), or prebuilt. Some places offer assembly service for you, though I think assembly is half the fun!</p> <h3 id="keyboard-size-and-shape" tabindex="-1">Keyboard size and shape</h3> <p>Your regular rectangular keyboard can have any number of keys, and some limited edition ones have a truly bizarre layout. I recommend the <a href="https://switchandclick.com/keyboard-sizes/">Switch &amp; Click keyboard size guide</a> for an overview of the most common ones.</p> <p>When it comes to choosing a size, think about what’s important to you. Do you use the numpad? Better get a compact or a full-sized one. Do you need the F-keys (go 75% or larger) or will you be all right with using a function key and the num row (60-65% will do)? Do you like pain (a 40% is for you)?</p> <p>I have a KBDFans Tofu84 (75%) at work, and an ikki68 (65%) at home. The F2 key gets a lot of use at work when I’m renaming variables!</p> <figure> <picture><source type="image/webp" srcset="https://localghost.dev/img/737JpGSme8-280.webp 280w, https://localghost.dev/img/737JpGSme8-640.webp 640w, https://localghost.dev/img/737JpGSme8-960.webp 960w" sizes="auto"><img loading="lazy" decoding="async" src="https://localghost.dev/img/737JpGSme8-280.jpeg" alt="The KBDFans Tofu65 keyboard with black aluminium case. Its keycaps are purple and black, and the alpha key legends have a gradient from turquoise to peach. It is in Mac ISO layout with a split left shift." width="960" height="718" srcset="https://localghost.dev/img/737JpGSme8-280.jpeg 280w, https://localghost.dev/img/737JpGSme8-640.jpeg 640w, https://localghost.dev/img/737JpGSme8-960.jpeg 960w" sizes="auto"></picture> <figcaption>KBDFans Tofu65 with DSA Magic Girl keycaps</figcaption> </figure> <p>Then there’s the <a href="https://splitkb.com/">split keyboards</a>, literally split down the middle and connected by a cable. They’re meant to be really great for wrist strain, much better for you posture-wise, though I haven’t tried one myself as I don’t have the energy to re-learn to type. They’re also excellent if you want to look like you come from, or travel regularly to, space.</p> <figure> <picture><source type="image/webp" srcset="https://localghost.dev/img/Pux_Z2Fj7I-280.webp 280w, https://localghost.dev/img/Pux_Z2Fj7I-640.webp 640w, https://localghost.dev/img/Pux_Z2Fj7I-960.webp 960w" sizes="auto"><img loading="lazy" decoding="async" src="https://localghost.dev/img/Pux_Z2Fj7I-280.jpeg" alt="A dark blue split keyboard, one side in front of the other. It has black keycaps with white legends." width="960" height="640" srcset="https://localghost.dev/img/Pux_Z2Fj7I-280.jpeg 280w, https://localghost.dev/img/Pux_Z2Fj7I-640.jpeg 640w, https://localghost.dev/img/Pux_Z2Fj7I-960.jpeg 960w" sizes="auto"></picture> <figcaption>Photo by <a href="https://unsplash.com/@peppytoad">Peppy Toad</a> on <a href="https://unsplash.com/photos/black-computer-keyboard-on-black-table-FR7DkhhW2oA">Unsplash</a> </figcaption> </figure> <p>Joking aside, they are supposed to be much better for you. A popular ergo keyboard is the Ergodox, which is actually open source: you can download the files for the PCB from <a href="https://github.com/Ergodox-io/">GitHub</a> and get it printed yourself, sourcing whichever compatible parts you like (or 3D printing them). There are also a lot of kits or pre-made versions if you prefer.</p> <p>Then there's macropads, tiny and adorable mini keyboards that are a great way to get used to building keyboards if you don't have much experience soldering. I've got a <a href="https://keeb.io/collections/bdn9-collection/products/bdn9-rev-2-3x3-9-key-macropad-rotary-encoder-and-rgb">keeb.io BDN9</a> and a mint-coloured Owlab Voice Mini which is the cutest thing. They both have rotary encoders (knobs) on them which make them 100% more satisfying, but unfortunately I keep forgetting what I've programmed the keys to do so I find I rarely use them.</p> <figure> <picture><source type="image/webp" srcset="https://localghost.dev/img/nroVLbkDqv-280.webp 280w, https://localghost.dev/img/nroVLbkDqv-640.webp 640w, https://localghost.dev/img/nroVLbkDqv-960.webp 960w" sizes="auto"><img loading="lazy" decoding="async" src="https://localghost.dev/img/nroVLbkDqv-280.jpeg" alt="An angled view of the BDN9 macropad. It is a black square with two rotary knobs at the top, and 7 keys that are pastel coloured with fruit legends. There is a green cable plugged into it." width="960" height="829" srcset="https://localghost.dev/img/nroVLbkDqv-280.jpeg 280w, https://localghost.dev/img/nroVLbkDqv-640.jpeg 640w, https://localghost.dev/img/nroVLbkDqv-960.jpeg 960w" sizes="auto"></picture> <figcaption>BDN9 with DSA Milkshake keycaps</figcaption> </figure> <h3 id="keyboard-layouts-iso-ansi-jis-ortholinear" tabindex="-1">Keyboard layouts: ISO, ANSI, JIS, ortholinear</h3> <p>The most common keyboard layout I’ve seen is the ANSI layout, which is the standard keyboard layout in the US. It’s characterised by its flat enter key, longer shift key on the left, and extra key above the enter key.<br> ISO is generally used in Europe, and has a delightfully chunky enter key that’s two rows high, with a smaller left shift key (two keys where ANSI has one).<br> JIS is the Japanese International Standard, and has an extra key next to backspace, an ISO-style enter key, and a smaller right shift key.<br> Mechkeys has an <a href="https://mechkeys.com/blogs/guide/understanding-different-physical-layouts-for-keyboards-ansi-vs-iso-vs-jis">article about these common layouts</a>, including some diagrams.</p> <p>I know plenty of folks in Europe who use ANSI layout simply because the keyboards are more common, but I much prefer ISO. Generally, keyboard kits and PCBs will say which layouts they support; often they support multiple configurations so you could even mix and match, say, an ISO enter key and a US-style left shift.</p> <p>Finally we have the <strong>ortholinear</strong> layout. These are quite amazing looking, a bit like a long <a href="https://en.wikipedia.org/wiki/Boggle">Boggle</a>. Unlike regular keyboards which have staggered rows, the keys are all aligned in a grid. <a href="https://www.tryorthokeys.com/ultimate-guide-to-ortholinear-keyboards">Read more about ortho keyboards</a> on Try Ortho Keys.</p> <h3 id="typing-angle" tabindex="-1">Typing angle</h3> <p>Perhaps a minor one, but this may make a difference to you; different keyboards are set at different angles, and unlike your 2001 Compaq keyboard most of these mechanical ones don’t have those little fold-out feet that you snapped off while waiting for the dialup to connect. Most keyboard listings will tell you the typing angle.</p> <h3 id="dampening-sound" tabindex="-1">Dampening sound</h3> <p>If you’re like me, you want to minimise the sound from a keyboard as much as possible, especially in the office. There are a few ways of doing that:</p> <ul> <li>use quieter switches (red, black)</li> <li>use <a href="https://mechswitcher.com/most-silent-linear-switches/">silent switches</a> (NB these are quieter, not actually silent)</li> <li>put <a href="https://mechkeyboardexpert.com/what-are-mechanical-keyboard-o-rings-everything-you-need-to-know/">O-rings</a> on your keycaps (I got some cheap off eBay)</li> <li>line your case with <a href="https://switchandclick.com/the-best-dampening-foam-for-a-mechanical-keyboard/">foam</a> to absorb sound</li> <li>put a desk mat underneath your keyboard</li> </ul> <h2 id="keycaps" tabindex="-1">Keycaps</h2> <p>The most exciting bit, I reckon. The bits with the letters on that are prettier than they have any right to be. You can get retro ones, cute pastel ones, dark ones with light writing... the possibilities are endless.</p> <p>Some of my favourite keysets of recent years:</p> <ul> <li><a href="https://novelkeys.com/products/dsa-milkshake">DSA Milkshake</a></li> <li><a href="https://vala.supply/products/epbt-dreamscape">ePBT Dreamscape</a></li> <li><a href="https://en.zfrontier.com/products/space-dust">KAT Space Dust</a> (this has been delayed forever, bah)</li> <li><a href="https://mintlodica.com/products/dsa-pastel-dreams-keycaps">Mintlodica Pastel Dreams</a></li> </ul> <figure> <picture><source type="image/webp" srcset="https://localghost.dev/img/pwmblb5PHD-280.webp 280w, https://localghost.dev/img/pwmblb5PHD-640.webp 640w, https://localghost.dev/img/pwmblb5PHD-960.webp 960w" sizes="auto"><img loading="lazy" decoding="async" src="https://localghost.dev/img/pwmblb5PHD-280.jpeg" alt="TA top-down view of a mechanical keyboard with ANSI layout. Its keycaps form a sort of gradient, colours in stripes going diagonally left to right from peach to pink to purple to blue to white." width="960" height="540" srcset="https://localghost.dev/img/pwmblb5PHD-280.jpeg 280w, https://localghost.dev/img/pwmblb5PHD-640.jpeg 640w, https://localghost.dev/img/pwmblb5PHD-960.jpeg 960w" sizes="auto"></picture> <figcaption>Render of Dreamscape keycaps from <a href="https://vala.supply/products/epbt-dreamscape">vala.supply</a></figcaption> </figure> <h3 id="kits" tabindex="-1">Kits</h3> <p>Keycaps will usually have different kits on sale, so check the listing carefully to see what each one contains. You'll definitely want to get the base kit (with all the alphanumeric keys and ANSI keys). Sometimes you have to buy the ISO kit separately for the big enter key and some Europe-specific keys like the UK 3 (# and £). Often sets will do a novelties kit: Milkshake had a set of pastel-coloured keys with fruit legends, and Dreamscape has novelties with stars, clouds and meteors. I always enjoy the novelties.</p> <h3 id="materials" tabindex="-1">Materials</h3> <p>Keycaps are generally plastic, except for the occasional novelty one. They’re either made of <a href="https://switchandclick.com/abs-vs-pbt-keycaps-whats-the-difference/">PBT or ABS</a>; PBT is nicer but ABS is cheaper. The sets you get with your off-the-shelf keyboard, or off AliExpress, are probably ABS. It’s more flexible, but the legends can wear off the surface of the keycaps.</p> <p>Additionally, keycaps may be <a href="https://teksbit.com/what-are-double-shot-keycaps-guide/">“double shot”</a> (two layers of plastic, one for the lettering and one for the overlay).</p> <p>Keycaps can come in a variety of different profiles, and may be flat (all one height) or sculpted (different rows have different heights). Different profiles have different heights. Generally I’m not fussy about profiles, I just accept whatever profile they’ve chosen for the keyset I’m after.</p> <p>Check out <a href="https://thekeeblog.com/overview-of-different-keycap-profiles/">The Keeblog’s article about different keycap profiles</a> for a full overview.</p> <h3 id="key-sizes" tabindex="-1">Key sizes</h3> <p>The keys themselves are measured in units (u): your average letter or number key is 1u wide, and everything else is relative to that. Most keysets will come with various different lengths for backspace, shift and caps lock keys which you might use depending on the layout of your keyboard. Some folks like a split spacebar; this is a must if you’ve got a split ergonomic keyboard, as you’ll need one on each side.</p> <p>Check out <a href="https://www.keyboard.university/100-courses/keycaps-101-ydy8j">Keyboard University’s guide to keycaps</a>.</p> <h2 id="how-to-actually-buy-keyboards-and-keycaps" tabindex="-1">How to actually buy keyboards and keycaps</h2> <p>You can buy most switches in large enough quantities from wherever (even eBay), but many keyboards or keycaps require a bit more effort.</p> <h3 id="the-group-buy-process" tabindex="-1">The group buy process</h3> <p>Since it’s a niche hobby, there isn’t enough demand for all of these things to just be mass-produced. Many keyboards, and most keysets, will go through a process called <strong>group buy</strong>, where a certain number of sets need to be sold before production can go ahead.</p> <p>Generally things will go through <strong>interest checks (ICs)</strong> before this stage, to gauge how much interest there is in the first place. Designers will produce very lifelike renders of the proposed keyset or keyboard, and if there’s enough interest they’ll approach a manufacturer. There will be a minimum order quantity, so they’ll have to guarantee a certain number of sales before production can go ahead. Production generally happens in China, and takes many months (especially if there are lots of sets in the queue in front of it).</p> <p>This is where the group buy phase begins. Various vendors around the world will agree to be the distributors for the set; this means you buy the set through the vendor, and the vendor in turn places an order for a certain number of sets from the manufacturer. The group buy phase has a limited time period, though usually there are no limits on how many units are available. A warning: generally there are <strong>no refunds</strong> on group buys because they need to guarantee the order quantity.</p> <p>Once the group buy is over, production can begin. The factory will produce a sample set which will go back to the designer to see if they’re happy with it, and this can happen a few times until the designer is happy. Finally, it’ll go into full production, and eventually distribution. The sets will be shipped to the vendor and they’ll be in charge of shipping them to their customers.</p> <p>Vendors should provide updates with how things are progressing, but there are often delays. Group buys generally have a ship date of a particular quarter rather than a specific month (e.g. Q3 2024) but this is subject to change.</p> <p>Most vendors will have a list of upcoming and current group buys on their website; see <a href="https://localghost.dev/blog/so-you-ve-decided-to-get-into-mechanical-keyboards/#where-to-buy">“Where to buy”</a> below.</p> <p>If you missed out on a group buy, don't despair. Sometimes creators will do re-runs or new revisions of popular keysets/keyboards.</p> <h3 id="what-to-buy-if-you-want-it-now" tabindex="-1">What to buy if you want it <em>now</em></h3> <p>If you genuinely don’t care about group buys and want something right away (fair play), consider checking out a <a href="https://www.keychron.com/">Keychron</a>. It’s a solid choice and readily available. It definitely doesn’t fulfil the “pretty” requirement, but it’s nice to type on and won’t set you back as much as the custom ones. You can swap out the keycaps and many of them are hot-swappable.</p> <p>A lot of keyboard retailers will have some keyboards and keysets in stock most of the time, including the “extras” that are additional sets they ordered from a group buy to sell on. These tend to go quite quickly, so if you missed a group buy make sure you pay attention to when the extras will be in stock and act fast.</p> <h2 id="where-to-buy" tabindex="-1">Where to buy</h2> <p>There’s a very comprehensive <a href="https://www.alexotos.com/keyboard-vendor-list/">list of keyboard vendors</a> by Alexotos. My personal (UK) faves are <a href="https://prototypist.net">Prototypist</a> (for group buys) and <a href="https://mechboards.co.uk">Mechboards</a> for in-stock kits and accessories. I buy switches from Mechboards or eBay.</p> <p>If you're not sure about a vendor, check out one of the mechanical keyboard discord servers and ask around for reviews. I find people are generally pretty helpful.</p> <p>As always, prepare to be hit by customs charges when ordering from abroad. That includes ordering from the EU to the UK (sob). Ordering from local vendors doesn't incur customs fees as these are shipped locally, though generally the customs charges will be factored into the group buy price.</p> <h2 id="further-reading-and-links" tabindex="-1">Further reading &amp; links</h2> <ul> <li><a href="https://www.keyboard.university/">Keyboard University</a> – a beginner’s guide to all things mechanical keyboards</li> <li><a href="https://mintlodica.com/pages/cutekeyboard-club-discord-server">Cute Keyboard Club</a> – a discord server about adorable mechanical keyboards and all sorts of other things</li> <li><a href="https://mintlodica.com/">Mintlodica</a> – one of my favourite creators of beautiful cute keycaps and keyboards</li> <li><a href="https://kbdfans.com/">KBDFans</a> make some of the best keyboards out there, including my personal favourite Tofu65. They're based in China, so watch out for customs fees.</li> </ul> Automated weekly links posts with raindrop.io and Eleventy 2024-01-22T00:00:00Z https://localghost.dev/blog/automated-weekly-links-posts-with-raindrop-io-and-eleventy/ <p><em>Edit 04/02/24 to change the comparator date from midnight on previous Saturday to midnight on previous Sunday to prevent duplicate links being published.</em></p> <p>A post that’s been getting a lot of traction recently is <a href="https://blog.cassidoo.co/post/human-curation/">I miss human curation</a> by Cassidy Williams, in which she laments that we’re so reliant on algorithms to show us new stuff now, instead of having it recommended to us by other humans.</p> <p>Inspired by that, I decided to start posting weekly collections of posts and links I liked that week. Knowing I wouldn’t keep it up if I had to manually post it every week, I set about finding a way to automate it. How could I mark a link or blog post as “good”, and have it show up in my blog on a Sunday without me having to do anything?</p> <p>It occurred to me that I’m already paying for <a href="http://raindrop.io">raindrop.io</a>, an excellent bookmark manager. It’s a great way of keeping links in sync across multiple platforms... and it has an API! This meant I could add links to a particular <a href="http://raindrop.io">raindrop.io</a> collection as I come across them, and then fetch them once a week and turn them into a post.</p> <h2 id="collecting-the-links" tabindex="-1">Collecting the links</h2> <p>I’ve created a separate <a href="http://raindrop.io">raindrop.io</a> collection for these links, which I can easily share to from the iOS share sheet, or from the Firefox extension. When I save the bookmark, I also add an accompanying note with a sentence or two about the link.</p> <figure><picture><source type="image/webp" srcset="https://localghost.dev/img/mMEmFF6QY1-280.webp 280w, https://localghost.dev/img/mMEmFF6QY1-640.webp 640w" sizes="auto"><img loading="lazy" decoding="async" src="https://localghost.dev/img/mMEmFF6QY1-280.jpeg" alt="A screenshot of a Firefox tab navigated to hidde.blog, with the raindrop.io extension visible. I am bookmarking a blog post by Hidde de Vries about his own link sharing plans, and I've written a note about it: 'Hidde's posted about sharing links on his own blog as well - great minds think alike!'. I have tagged the post 'good links'." width="640" height="488" srcset="https://localghost.dev/img/mMEmFF6QY1-280.jpeg 280w, https://localghost.dev/img/mMEmFF6QY1-640.jpeg 640w" sizes="auto"></picture></figure> <p>I generated an API token for raindrop, and wrote a little script to pull the links from the collection using the <code>/raindrops/[collectionID]</code> <a href="https://developer.raindrop.io/v1/raindrops/multiple">endpoint</a>.</p> <p>I made sure to only fetch links from the past week so I didn’t duplicate anything. You can pass specific <a href="https://help.raindrop.io/using-search/#operators">search parameters</a> in the query, so I restricted the links to any created <em>after</em> midnight on the previous Sunday, and <em>before</em> midnight on the current day - so, Sunday to Saturday. That means any links I clip on the Sunday will appear in the following week’s link post.</p> <pre class="language-js"><code class="language-js"><span class="token keyword">const</span> todayDate <span class="token operator">=</span> <span class="token keyword">new</span> <span class="token class-name">Date</span><span class="token punctuation">(</span><span class="token punctuation">)</span><span class="token punctuation">;</span> <span class="token keyword">const</span> lastSunDate <span class="token operator">=</span> <span class="token function">subDays</span><span class="token punctuation">(</span>todayDate<span class="token punctuation">,</span> <span class="token number">7</span><span class="token punctuation">)</span><span class="token punctuation">;</span> <span class="token comment">// using date-fns here</span> <span class="token keyword">const</span> lastSun <span class="token operator">=</span> <span class="token function">format</span><span class="token punctuation">(</span>lastSunDate<span class="token punctuation">,</span> <span class="token string">"yyyy-MM-dd"</span><span class="token punctuation">)</span><span class="token punctuation">;</span> <span class="token keyword">const</span> today <span class="token operator">=</span> <span class="token function">format</span><span class="token punctuation">(</span>todayDate<span class="token punctuation">,</span> <span class="token string">"yyyy-MM-dd"</span><span class="token punctuation">)</span><span class="token punctuation">;</span> <span class="token keyword">async</span> <span class="token keyword">function</span> <span class="token function">fetchLinks</span><span class="token punctuation">(</span><span class="token punctuation">)</span> <span class="token punctuation">{</span> <span class="token comment">// Get content bookmarked between last Sunday and this Saturday inclusive</span> <span class="token keyword">const</span> search <span class="token operator">=</span> <span class="token keyword">new</span> <span class="token class-name">URLSearchParams</span><span class="token punctuation">(</span><span class="token punctuation">{</span> <span class="token literal-property property">search</span><span class="token operator">:</span> <span class="token template-string"><span class="token template-punctuation string">`</span><span class="token string">created:></span><span class="token interpolation"><span class="token interpolation-punctuation punctuation">${</span>lastSun<span class="token interpolation-punctuation punctuation">}</span></span><span class="token string"> created:&lt;</span><span class="token interpolation"><span class="token interpolation-punctuation punctuation">${</span>today<span class="token interpolation-punctuation punctuation">}</span></span><span class="token template-punctuation string">`</span></span><span class="token punctuation">,</span> <span class="token punctuation">}</span><span class="token punctuation">)</span><span class="token punctuation">;</span> <span class="token keyword">const</span> url <span class="token operator">=</span> <span class="token keyword">new</span> <span class="token class-name">URL</span><span class="token punctuation">(</span><span class="token template-string"><span class="token template-punctuation string">`</span><span class="token string">https://api.raindrop.io/rest/v1/raindrops/</span><span class="token interpolation"><span class="token interpolation-punctuation punctuation">${</span>collectionId<span class="token interpolation-punctuation punctuation">}</span></span><span class="token template-punctuation string">`</span></span><span class="token punctuation">)</span><span class="token punctuation">;</span> url<span class="token punctuation">.</span>search <span class="token operator">=</span> search<span class="token punctuation">;</span> <span class="token keyword">const</span> rsp <span class="token operator">=</span> <span class="token keyword">await</span> <span class="token function">fetch</span><span class="token punctuation">(</span>url<span class="token punctuation">,</span> <span class="token punctuation">{</span> <span class="token literal-property property">headers</span><span class="token operator">:</span> <span class="token punctuation">{</span> <span class="token literal-property property">Authorization</span><span class="token operator">:</span> <span class="token template-string"><span class="token template-punctuation string">`</span><span class="token string">Bearer </span><span class="token interpolation"><span class="token interpolation-punctuation punctuation">${</span>token<span class="token interpolation-punctuation punctuation">}</span></span><span class="token template-punctuation string">`</span></span><span class="token punctuation">,</span> <span class="token punctuation">}</span><span class="token punctuation">,</span> <span class="token punctuation">}</span><span class="token punctuation">)</span><span class="token punctuation">;</span> <span class="token keyword">return</span> <span class="token keyword">await</span> rsp<span class="token punctuation">.</span><span class="token function">json</span><span class="token punctuation">(</span><span class="token punctuation">)</span><span class="token punctuation">;</span> <span class="token punctuation">}</span></code></pre> <h2 id="creating-the-post" tabindex="-1">Creating the post</h2> <p>Once I’ve pulled the links, I need to turn them into an actual markdown post. I’ve created a very simple template that I can inject content into:</p> <pre class="language-md"><code class="language-md"><span class="token front-matter-block"><span class="token punctuation">---</span> <span class="token front-matter yaml language-yaml">date: {{date}}</span> <span class="token punctuation">---</span></span> {{links}}</code></pre> <p>Using my method for creating <a href="https://localghost.dev/blog/building-post-types-and-category-rss-feeds-in-eleventy/">post types</a>, I've added a new <code>link</code> type which has its own shared config. I'm using a couple of custom date filters to get the dates in the right format for titles and URLs.</p> <pre class="language-json"><code class="language-json"><span class="token punctuation">{</span> <span class="token property">"layout"</span><span class="token operator">:</span> <span class="token string">"single-post.njk"</span><span class="token punctuation">,</span> <span class="token property">"hasCustomOGImage"</span><span class="token operator">:</span> <span class="token boolean">true</span><span class="token punctuation">,</span> <span class="token property">"eleventyComputed"</span><span class="token operator">:</span> <span class="token punctuation">{</span> <span class="token property">"title"</span><span class="token operator">:</span> <span class="token string">"Good links: {{ date | dateFilter }}"</span> <span class="token punctuation">}</span><span class="token punctuation">,</span> <span class="token property">"excerptText"</span><span class="token operator">:</span> <span class="token string">"Links to posts and websites I've enjoyed this week, curated and automated."</span><span class="token punctuation">,</span> <span class="token property">"type"</span><span class="token operator">:</span> <span class="token string">"link"</span><span class="token punctuation">,</span> <span class="token property">"tags"</span><span class="token operator">:</span> <span class="token punctuation">[</span> <span class="token string">"links"</span> <span class="token punctuation">]</span><span class="token punctuation">,</span> <span class="token property">"permalink"</span><span class="token operator">:</span> <span class="token string">"/blog/good-links-{{date | urlDateFilter }}/index.html"</span> <span class="token punctuation">}</span></code></pre> <p>I format the links into markdown, defaulting to raindrop’s excerpt if I didn’t write a note:</p> <pre class="language-js"><code class="language-js"><span class="token keyword">const</span> formattedLinks <span class="token operator">=</span> raindrops<span class="token punctuation">.</span><span class="token function">map</span><span class="token punctuation">(</span><span class="token punctuation">(</span><span class="token parameter">raindrop</span><span class="token punctuation">)</span> <span class="token operator">=></span> <span class="token punctuation">{</span> <span class="token keyword">const</span> <span class="token punctuation">{</span> link<span class="token punctuation">,</span> title<span class="token punctuation">,</span> excerpt<span class="token punctuation">,</span> note <span class="token punctuation">}</span> <span class="token operator">=</span> raindrop<span class="token punctuation">;</span> <span class="token keyword">const</span> description <span class="token operator">=</span> note <span class="token operator">===</span> <span class="token string">""</span> <span class="token operator">?</span> excerpt <span class="token operator">:</span> note<span class="token punctuation">;</span> <span class="token keyword">return</span> <span class="token template-string"><span class="token template-punctuation string">`</span><span class="token string">* [</span><span class="token interpolation"><span class="token interpolation-punctuation punctuation">${</span>title<span class="token interpolation-punctuation punctuation">}</span></span><span class="token string">](</span><span class="token interpolation"><span class="token interpolation-punctuation punctuation">${</span>link<span class="token interpolation-punctuation punctuation">}</span></span><span class="token string">) - </span><span class="token interpolation"><span class="token interpolation-punctuation punctuation">${</span>description<span class="token interpolation-punctuation punctuation">}</span></span><span class="token template-punctuation string">`</span></span><span class="token punctuation">;</span> <span class="token punctuation">}</span><span class="token punctuation">)</span><span class="token punctuation">;</span></code></pre> <p>Then I read the template as a string, interpolate the date and links I’ve just formatted, and write them to a file in my blog directory with the date as a filename.</p> <pre class="language-js"><code class="language-js"> <span class="token keyword">let</span> postContent <span class="token operator">=</span> fs<span class="token punctuation">.</span><span class="token function">readFileSync</span><span class="token punctuation">(</span><span class="token string">"./scripts/link_template.md"</span><span class="token punctuation">,</span> <span class="token string">"utf8"</span><span class="token punctuation">)</span><span class="token punctuation">;</span> postContent <span class="token operator">=</span> postContent<span class="token punctuation">.</span><span class="token function">replace</span><span class="token punctuation">(</span><span class="token string">"{{date}}"</span><span class="token punctuation">,</span> formattedToday<span class="token punctuation">)</span><span class="token punctuation">;</span> postContent <span class="token operator">=</span> postContent<span class="token punctuation">.</span><span class="token function">replace</span><span class="token punctuation">(</span><span class="token string">"{{links}}"</span><span class="token punctuation">,</span> formattedLinks<span class="token punctuation">.</span><span class="token function">join</span><span class="token punctuation">(</span><span class="token string">"\n"</span><span class="token punctuation">)</span><span class="token punctuation">)</span><span class="token punctuation">;</span> fs<span class="token punctuation">.</span><span class="token function">writeFileSync</span><span class="token punctuation">(</span><span class="token template-string"><span class="token template-punctuation string">`</span><span class="token string">./src/blog/links/</span><span class="token interpolation"><span class="token interpolation-punctuation punctuation">${</span>formattedToday<span class="token interpolation-punctuation punctuation">}</span></span><span class="token string">.md</span><span class="token template-punctuation string">`</span></span><span class="token punctuation">,</span> postContent<span class="token punctuation">)</span><span class="token punctuation">;</span></code></pre> <h2 id="scheduling-the-script" tabindex="-1">Scheduling the script</h2> <p>Running the script is easy enough: I added it into my <code>package.json</code> scripts as <code>yarn generate-links</code>. But I’d like to not even have to think about running it and have something do it for me.</p> <p>It made sense to use GitHub Actions to run it on a schedule. I already deploy my website twice a day automatically so that new webmentions are fetched regularly.</p> <p>I randomly entered numbers into <a href="https://crontab.guru/">crontab.guru</a> until it came out with “every Sunday at 6pm”, and then created a new workflow:</p> <pre class="language-yaml"><code class="language-yaml"><span class="token key atrule">name</span><span class="token punctuation">:</span> <span class="token string">"Generate and publish weekly link post"</span> <span class="token key atrule">on</span><span class="token punctuation">:</span> <span class="token key atrule">schedule</span><span class="token punctuation">:</span> <span class="token punctuation">-</span> <span class="token key atrule">cron</span><span class="token punctuation">:</span> 0 18 * * 0 <span class="token key atrule">jobs</span><span class="token punctuation">:</span> <span class="token key atrule">generate-post</span><span class="token punctuation">:</span> <span class="token key atrule">name</span><span class="token punctuation">:</span> <span class="token string">"Run script to generate post"</span> <span class="token key atrule">runs-on</span><span class="token punctuation">:</span> ubuntu<span class="token punctuation">-</span>latest <span class="token key atrule">steps</span><span class="token punctuation">:</span> <span class="token punctuation">-</span> <span class="token key atrule">uses</span><span class="token punctuation">:</span> actions/checkout@v4 <span class="token punctuation">-</span> <span class="token key atrule">uses</span><span class="token punctuation">:</span> actions/setup<span class="token punctuation">-</span>node@v3 <span class="token key atrule">with</span><span class="token punctuation">:</span> <span class="token key atrule">node-version</span><span class="token punctuation">:</span> <span class="token string">"18"</span> <span class="token key atrule">cache</span><span class="token punctuation">:</span> <span class="token string">"yarn"</span> <span class="token punctuation">-</span> <span class="token key atrule">run</span><span class="token punctuation">:</span> yarn install <span class="token punctuation">-</span> <span class="token key atrule">run</span><span class="token punctuation">:</span> yarn generate<span class="token punctuation">-</span>links <span class="token key atrule">env</span><span class="token punctuation">:</span> <span class="token key atrule">RAINDROP_TOKEN</span><span class="token punctuation">:</span> $<span class="token punctuation">{</span><span class="token punctuation">{</span> secrets.RAINDROP_TOKEN <span class="token punctuation">}</span><span class="token punctuation">}</span> <span class="token key atrule">RAINDROP_COLLECTION_ID</span><span class="token punctuation">:</span> $<span class="token punctuation">{</span><span class="token punctuation">{</span> secrets.RAINDROP_COLLECTION_ID <span class="token punctuation">}</span><span class="token punctuation">}</span> <span class="token punctuation">-</span> <span class="token key atrule">uses</span><span class="token punctuation">:</span> stefanzweifel/git<span class="token punctuation">-</span>auto<span class="token punctuation">-</span>commit<span class="token punctuation">-</span>action@v5 <span class="token key atrule">with</span><span class="token punctuation">:</span> <span class="token key atrule">commit_message</span><span class="token punctuation">:</span> Generate weekly link post</code></pre> <p>I'm storing the <a href="http://raindrop.io">raindrop.io</a> token and collection ID in the repository secrets.</p> <p>My script generates a new <code>.md</code> file with the post, so I need it to commit and push the changes. For this I used <a href="https://github.com/stefanzweifel/git-auto-commit-action">git-auto-commit-action</a>. It detects changed files during a workflow run and commits and pushes them back to the repo.</p> <p>My <a href="https://github.com/sophiekoonin/localghost/blob/main/.github/workflows/deploy-neocities.yml">deployment workflow</a> listens for the link creation workflow to complete, so once this workflow finishes running and pushes the changes, the deployment one will kick off and deploy my new blog post. Magic!</p> <pre class="language-yaml"><code class="language-yaml"><span class="token comment"># deploy-neocities.yml</span> <span class="token key atrule">name</span><span class="token punctuation">:</span> <span class="token string">"Deploy to Neocities"</span> <span class="token key atrule">on</span><span class="token punctuation">:</span> <span class="token punctuation">[</span><span class="token punctuation">...</span><span class="token punctuation">]</span> <span class="token key atrule">workflow_run</span><span class="token punctuation">:</span> <span class="token key atrule">workflows</span><span class="token punctuation">:</span> <span class="token punctuation">[</span>Generate and publish weekly link post<span class="token punctuation">]</span> <span class="token key atrule">types</span><span class="token punctuation">:</span> <span class="token punctuation">-</span> completed</code></pre> Good links: 21 January 2024 2024-01-21T00:00:00Z https://localghost.dev/blog/good-links-2024-01-21/ <ul> <li><a href="https://aftermath.site/the-internet-is-full-of-ai-dogshit">The Internet Is Full of AI Dogshit - Aftermath</a> - How AI-generated content is ruining search engines for everyone.</li> <li><a href="https://chriscoyier.net/2024/01/13/exposed-rss/">Exposed RSS</a> - I get sites not having an “RSS” for “Feed” link on their website while actually having an RSS feed. I don’t like it, but I get it. Maybe they picked an off-the-shelf t…</li> <li><a href="https://keith.is/posts/you-should-blog/">You should blog | keith.is</a> - Keith calls out for folks to make this the year of blogging about whatever the hell you want. He’s suggested some great places to get started!</li> <li><a href="https://lethain.com/layers-of-context/">Layers of context.</a> - Will Larson on the importance of thinking outside of your immediate context: why a change that seems like a great idea may not be so well received elsewhere in the organisation, and how you can get a better radar for these things.</li> <li><a href="https://blog.cassidoo.co/post/annoyed-at-react/">Kind of annoyed at React</a> - Cassidy Williams shares her frustrations about React, echoing some of the things I’ve been feeling recently.</li> <li><a href="https://svgfm.chriskirknielsen.com/">SVG Filter Maker</a> - SVGFM, a node graph builder for SVG filters</li> <li><a href="https://hidde.blog/sharing-links/">Sharing links</a> - Hidde's posted about sharing links on his own blog as well - great minds think alike!</li> </ul> How I deploy my Eleventy site to Neocities 2024-01-20T00:00:00Z https://localghost.dev/blog/how-i-deploy-my-eleventy-site-to-neocities/ <p>Skip to the bit you care about:</p> <ul> <li><a href="https://localghost.dev/blog/how-i-deploy-my-eleventy-site-to-neocities/#about-neocities">About Neocities</a></li> <li><a href="https://localghost.dev/blog/how-i-deploy-my-eleventy-site-to-neocities/#hosting-a-static-site-on-neocities">Hosting a static site on Neocities</a></li> <li><a href="https://localghost.dev/blog/how-i-deploy-my-eleventy-site-to-neocities/#continuous-deployment-to-neocities-with-github-actions">Continuous deployment to Neocities with Github Actions</a> <ul> <li><a href="https://localghost.dev/blog/how-i-deploy-my-eleventy-site-to-neocities/#scheduling-builds">Scheduling builds</a></li> </ul> </li> </ul> <p>I’ve hosted this website in a few different places since I started it in 2019. It started out on Netlify, but after my post about everything I googled went viral, I exceeded the bandwidth of the free plan. Then I moved it to Vercel, which (at least at the time) had more generous bandwidth on the free tier. I enjoyed very quick deployment speeds, but it gave me big Corporate Web vibes. Now I host localghost.dev on <a href="https://neocities.org">Neocities</a>.</p> <h2 id="about-neocities" tabindex="-1">About Neocities</h2> <p>Neocities reminds me of everything I love about the old web. It’s a place for people to create <a href="https://neocities.org/browse">bizarre, oddly specific websites</a> for things they love, or random collections of digital trinkets they’ve found on their online travels.</p> <p>The manifesto on their <a href="https://neocities.org/about">about page</a> resonated with me:</p> <blockquote> <p>We are tired of living in an online world where people are isolated from each other on boring, generic social networks that don't let us truly express ourselves. <strong>It's time we took back our personalities from these sterilized, lifeless, monetized, data mined, monitored addiction machines and let our creativity flourish again.</strong></p> </blockquote> <h2 id="hosting-a-static-site-on-neocities" tabindex="-1">Hosting a static site on Neocities</h2> <p>Neocities offers free static site hosting, and (unlike the free hosts of yore) there are NO ADS. None. It’s been around for years, so it’s not going anywhere any time soon.</p> <p>You can manually build your site and upload the assets via the web editor, if you want. However, I’d recommend using version control so that you can easily roll back changes, and move your site somewhere else if you need to. There’s a <a href="https://neocities.org/cli">CLI tool</a> and an <a href="https://neocities.org/api">API</a> that make it much easier to upload new versions of your site.</p> <p>The <a href="https://neocities.org/supporter">supporter plan</a> allows you to use custom domains, gives you loads more storage and bandwidth, and allows as many sites as you want under the same account. It costs $5 a month, which really isn’t very much, and I’m happy to pay for it knowing that it’s supporting people creating websites for free and learning web development the same way I did.</p> <h2 id="continuous-deployment-to-neocities-with-github-actions" tabindex="-1">Continuous deployment to Neocities with Github Actions</h2> <p>Like with Netlify and Vercel, I wanted to be able to set up a pipeline to automatically deploy my site when I pushed to the main branch. I’ve used CircleCI in the past, but I wanted to use <a href="https://docs.github.com/en/actions/learn-github-actions/understanding-github-actions">Github Actions</a> as it’s just... nicer.</p> <p>I was very excited to discover that there’s a <a href="https://github.com/bcomnes/deploy-to-neocities">GitHub Action for deploying to Neocities</a> by Bret Comnes! It uses the Neocities API under the hood, so you need to create an API key.</p> <p>You can do this via the API (this example is from the API docs):</p> <pre class="language-sh"><code class="language-sh">$ <span class="token function">curl</span> <span class="token string">"https://USER:[email protected]/api/key"</span> <span class="token punctuation">{</span> <span class="token string">"result"</span><span class="token builtin class-name">:</span> <span class="token string">"success"</span>, <span class="token string">"api_key"</span><span class="token builtin class-name">:</span> <span class="token string">"da77c3530c30593663bf7b797323e48c"</span> <span class="token punctuation">}</span></code></pre> <p>Or you can create a new key from your site settings page at <code>https://neocities.org/settings/&lt;your username&gt;#api_key</code>.</p> <p>Once you’ve got the key, store it in your repo’s actions secrets. Head to your repo’s settings page, and open “Secrets and Variables” &gt; “Actions” under the “Security” heading. Create a new repository secret called <code>NEOCITIES_TOKEN</code> and set its value as the key you just created.</p> <p>If you don’t have any other GH Actions workflows set up, you’ll need to create the <code>.github/workflows</code> directory. In here, I’ve got a workflow called <code>deploy-neocities.yml</code>.</p> <p>I’ve added some comments to show you what each bit does.</p> <pre class="language-yaml"><code class="language-yaml"><span class="token key atrule">name</span><span class="token punctuation">:</span> <span class="token string">"Deploy to Neocities"</span> <span class="token key atrule">on</span><span class="token punctuation">:</span> <span class="token key atrule">push</span><span class="token punctuation">:</span> <span class="token key atrule">branches</span><span class="token punctuation">:</span> <span class="token punctuation">-</span> main <span class="token comment"># run this job when I push to main</span> <span class="token key atrule">jobs</span><span class="token punctuation">:</span> <span class="token key atrule">deploy</span><span class="token punctuation">:</span> <span class="token key atrule">name</span><span class="token punctuation">:</span> <span class="token string">"Deploy to Neocities"</span> <span class="token key atrule">runs-on</span><span class="token punctuation">:</span> ubuntu<span class="token punctuation">-</span>latest <span class="token comment"># the system the runner uses</span> <span class="token key atrule">steps</span><span class="token punctuation">:</span> <span class="token punctuation">-</span> <span class="token key atrule">uses</span><span class="token punctuation">:</span> actions/checkout@v4 <span class="token comment"># checkout the repo</span> <span class="token punctuation">-</span> <span class="token key atrule">uses</span><span class="token punctuation">:</span> actions/setup<span class="token punctuation">-</span>node@v3 <span class="token comment"># install node.js</span> <span class="token key atrule">with</span><span class="token punctuation">:</span> <span class="token key atrule">node-version</span><span class="token punctuation">:</span> <span class="token string">"18"</span> <span class="token key atrule">cache</span><span class="token punctuation">:</span> <span class="token string">"yarn"</span> <span class="token comment"># I'm using yarn rather than npm</span> <span class="token punctuation">-</span> <span class="token key atrule">run</span><span class="token punctuation">:</span> yarn install <span class="token comment"># install dependencies</span> <span class="token punctuation">-</span> <span class="token key atrule">run</span><span class="token punctuation">:</span> yarn build <span class="token comment"># build my project</span> <span class="token key atrule">env</span><span class="token punctuation">:</span> <span class="token comment"># the env vars I need to build my site</span> <span class="token key atrule">WEBMENTION_IO_TOKEN</span><span class="token punctuation">:</span> $<span class="token punctuation">{</span><span class="token punctuation">{</span> secrets.WEBMENTION_IO_TOKEN <span class="token punctuation">}</span><span class="token punctuation">}</span> <span class="token punctuation">-</span> <span class="token key atrule">uses</span><span class="token punctuation">:</span> bcomnes/deploy<span class="token punctuation">-</span>to<span class="token punctuation">-</span>neocities@v1 <span class="token comment"># deploy!</span> <span class="token key atrule">with</span><span class="token punctuation">:</span> <span class="token comment"># config for the action</span> <span class="token key atrule">api_token</span><span class="token punctuation">:</span> $<span class="token punctuation">{</span><span class="token punctuation">{</span> secrets.NEOCITIES_TOKEN <span class="token punctuation">}</span><span class="token punctuation">}</span> <span class="token key atrule">dist_dir</span><span class="token punctuation">:</span> <span class="token string">"_site/"</span> <span class="token comment"># my build output directory</span> <span class="token key atrule">cleanup</span><span class="token punctuation">:</span> <span class="token boolean important">true</span> <span class="token comment"># delete anything on neocities that's not in my dist_dir</span></code></pre> <p>The workflow runs every time I push commits to the main branch.</p> <p>The environment variables you need for the <code>yarn build</code> step will vary, and you might not need any at all.</p> <p>You <em>will</em> need the <code>NEOCITIES_TOKEN</code> for the <code>deploy-to-neocities</code> action, which we pass in here as the <code>api_token</code> param.</p> <p>The default value of the <code>cleanup</code> param is <code>false</code>, and I had it set to that for ages, but then I realised that if I accidentally published a post and unpublished it by setting it back to draft, it wouldn’t delete the built post on Neocities. So even if it wasn’t linked from the site, the page itself would still be hosted there. I’ve since set <code>cleanup</code> to <code>true</code> so it removes any files from Neocities that aren’t in my newly built site output.</p> <p>Once you’ve got that workflow file finished, commit it and push it to your repo. GitHub will pick it up, and start running it automatically depending on what you told it to do. I find it goes pretty fast – my site deploys in about 30s.</p> <h3 id="scheduling-builds" tabindex="-1">Scheduling builds</h3> <p>Branch pushes aren’t the only triggers for workflows! You can have time-based triggers as well, using cron, the scheduling tool.</p> <p>For example, I automatically rebuild my site at 8am and 6pm every day to fetch the latest webmentions (there’s a script that runs every time the site is built).</p> <pre class="language-yaml"><code class="language-yaml"><span class="token key atrule">on</span><span class="token punctuation">:</span> <span class="token key atrule">schedule</span><span class="token punctuation">:</span> <span class="token punctuation">-</span> <span class="token key atrule">cron</span><span class="token punctuation">:</span> 0 8/9 * * * </code></pre> <p>I recommend <a href="https://crontab.guru">crontab.guru</a> for help with cron syntax.</p> Sending webmentions from a static site 2024-01-20T00:00:00Z https://localghost.dev/blog/sending-webmentions-from-a-static-site/ <p>It occurred to me this week that even though I’ve been using webmentions as comments for a really long time, I hadn’t actually been sending them myself. The shame! Here's how I set it up.</p> <p><em><strong>Edit 22/01/23:</strong> I've added in some checks to make sure I don't send webmentions for a post if I've already done it before.</em></p> <p>Certified good egg <a href="https://remysharp.com">Remy Sharp</a> has a tool called <a href="https://webmention.app">webmention.app</a> which will send webmentions for you – you CURL it with the URL of your post, and it’ll search through it for any links and check to see if the owners of those sites have webmentions or pingbacks set up. There’s also an npm package, <a href="https://www.npmjs.com/package/@remy/webmention">@remy/webmention</a>, which you give an RSS feed and it does the same thing. That’s the version I’ve used.</p> <p>You can add it as the <code>postbuild</code> script in <code>package.json</code>, so it’ll run after every <code>yarn build</code>.</p> <pre class="language-json"><code class="language-json"><span class="token property">"scripts"</span><span class="token operator">:</span> <span class="token punctuation">{</span> <span class="token property">"postbuild"</span><span class="token operator">:</span> <span class="token string">"webmention _site/feed.xml --limit 1 --send"</span> <span class="token punctuation">}</span><span class="token punctuation">,</span></code></pre> <p>I was concerned that if my build runs every 12 hours, it’ll keep sending webmentions for the same posts. Remy assures me that duplicate webmentions aren’t an issue, as the accepting server will just respond with a 200 if I send a webmention that it’s already seen. However, if you build your site very often you might be at risk of nearly DOS-ing the websites you're sending mentions to. To get around this, you could:</p> <ul> <li>write a script that runs the webmention CLI but only after checking the post timestamp of the last post to see if it's recent enough</li> <li>keep a record somehow of the last post you sent webmentions for, e.g. in a text file in the repo</li> <li>check the message from the last commit and only send the webmentions if you mention &quot;new post&quot; or similar</li> </ul> <p>To be super safe, I've added it as a script in <code>package.json</code> but I'm calling it from within a bash script that checks the commit hash for the last commit, and records whether or not I've sent webmentions for that commit already.</p> <pre class="language-sh"><code class="language-sh"><span class="token shebang important">#! /usr/bin/env bash</span> <span class="token keyword">if</span> <span class="token operator">!</span> <span class="token builtin class-name">test</span> <span class="token parameter variable">-f</span> ./last_webmentions_commit<span class="token punctuation">;</span> <span class="token keyword">then</span> <span class="token builtin class-name">echo</span> <span class="token string">"Last webmention commit file not found, exiting"</span> <span class="token builtin class-name">exit</span> <span class="token number">1</span> <span class="token keyword">fi</span> <span class="token comment"># Read the contents of the file</span> <span class="token assign-left variable">LAST_COMMIT_SENT</span><span class="token operator">=</span><span class="token variable"><span class="token variable">$(</span><span class="token function">cat</span> ./last_webmentions_commit<span class="token variable">)</span></span> <span class="token comment"># Get latest commit hash</span> <span class="token assign-left variable">LATEST_COMMIT</span><span class="token operator">=</span><span class="token variable"><span class="token variable">$(</span><span class="token function">git</span> rev-parse <span class="token parameter variable">--short</span> HEAD<span class="token variable">)</span></span> <span class="token comment"># If the last commit hash I sent webmentions for is</span> <span class="token comment"># the same as the latest commit hash, exit</span> <span class="token keyword">if</span> <span class="token punctuation">[</span><span class="token punctuation">[</span> <span class="token variable">$LAST_COMMIT_SENT</span> <span class="token operator">==</span> <span class="token variable">$LATEST_COMMIT</span> <span class="token punctuation">]</span><span class="token punctuation">]</span><span class="token punctuation">;</span> <span class="token keyword">then</span> <span class="token builtin class-name">echo</span> <span class="token string">"No new commits since we last sent webmentions, nothing to do"</span> <span class="token builtin class-name">exit</span> <span class="token number">0</span> <span class="token keyword">fi</span> <span class="token comment"># Get the last commit message</span> <span class="token assign-left variable">LAST_COMMIT_MESSAGE</span><span class="token operator">=</span><span class="token variable"><span class="token variable">$(</span><span class="token function">git</span> log <span class="token parameter variable">-1</span> <span class="token parameter variable">--pretty</span><span class="token operator">=</span>%B<span class="token variable">)</span></span> <span class="token comment"># Does the commit message contain 'post'?</span> <span class="token keyword">if</span> <span class="token operator">!</span> <span class="token punctuation">[</span><span class="token punctuation">[</span> <span class="token variable">$LAST_COMMIT_MESSAGE</span> <span class="token operator">==</span> *<span class="token string">"post"</span>* <span class="token punctuation">]</span><span class="token punctuation">]</span><span class="token punctuation">;</span> <span class="token keyword">then</span> <span class="token builtin class-name">echo</span> <span class="token string">"Last commit message does not contain 'post', nothing to do"</span> <span class="token builtin class-name">exit</span> <span class="token number">0</span> <span class="token keyword">fi</span> <span class="token comment"># Update the file with the latest commit hash</span> <span class="token builtin class-name">echo</span> <span class="token variable">$LATEST_COMMIT</span> <span class="token operator">></span> ./last_webmentions_commit <span class="token function">yarn</span> send-webmentions</code></pre> <p>When I write a new post, I always use the word &quot;post&quot; in the commit message, so this will help distinguish which commits we need to send webmentions for.</p> <p>Now, every time I build and deploy my site, the script will send webmentions to anyone I’ve mentioned in the last post I wrote, as long as I haven't already sent webmentions for that post and the recipients have the webmention meta tags set up!</p> Just because you can doesn't mean you should: the <meter> element 2024-01-07T00:00:00Z https://localghost.dev/blog/just-because-you-can-doesnt-mean-you-should-the-meter-element/ <p>I came across Sara Joy's (very cool) demo of <a href="https://codepen.io/sarajw/details/xxBGmRZ">CSS theming without classes</a> today, and looking through the code spotted a couple of elements I hadn't come across before: <code>&lt;progress&gt;</code> and <code>&lt;meter&gt;</code>. Granted, I've probably seen the <code>&lt;progress&gt;</code> element plenty of times, but I struggled to see what the <code>&lt;meter&gt;</code> was for.</p> <p>While <code>&lt;progress&gt;</code> is fairly self-explanatory – it shows how far along something is, such as your progress through a form – the <code>&lt;meter&gt;</code> element was less obvious to me, so I had a look at the MDN page to see what they suggested.</p> <figure><picture><source type="image/webp" srcset="https://localghost.dev/img/0F5j0SRba--280.webp 280w, https://localghost.dev/img/0F5j0SRba--640.webp 640w, https://localghost.dev/img/0F5j0SRba--960.webp 960w" sizes="auto"><img loading="lazy" decoding="async" src="https://localghost.dev/img/0F5j0SRba--280.jpeg" alt="A screenshot from MDN. HTML: &lt;p&gt; Heat the oven to &lt;meter min=&quot;200&quot; max=&quot;500&quot; value=&quot;350&quot;&gt;350 degrees&lt;/meter&gt;. &lt;/p&gt; Result: &quot;Heat the oven to&quot; and then a half-full progress bar that doesn't show you any indication of the actual number." width="960" height="557" srcset="https://localghost.dev/img/0F5j0SRba--280.jpeg 280w, https://localghost.dev/img/0F5j0SRba--640.jpeg 640w, https://localghost.dev/img/0F5j0SRba--960.jpeg 960w" sizes="auto"></picture></figure> <p>This is a prime example of <strong>following the letter, not the spirit, of semantic HTML</strong>. Yes, technically the cooking temperature is somewhere between the lowest and highest temperature you can set the oven to, but is this actually helping people understand the recipe? Quite the opposite, it's making the recipe less accessible for anyone <em>not</em> using a screen reader.</p> <p>Now, chances are the person who wrote this article simply couldn't think of a better example, and isn't necessarily proposing that everyone starts using meters instead of numbers in their recipes, but a <em>lot</em> of developers rely on MDN to tell them what is good practice, so I don't think this is particularly useful. (I'm going to try and contribute a better example.)</p> <p>Inspired by this, and with a burning desire to build something truly terrible, I've created this recipe page using the <code>&lt;meter&gt;</code> element for every numerical value.</p> <p class="codepen" data-height="265" data-theme-id="dark" data-default-tab="result" data-user="sophiekoonin" data-slug-hash="ZEPWxLL" data-preview="true" data-pen-title="CodePen Home A recipe, but all the numbers are <meter> bars"> <span>See the Pen <a href="https://codepen.io/sophiekoonin/pen/ZEPWxLL"> CodePen Home A recipe, but all the numbers are <meter> bars</meter></a> by <a href="https://codepen.io/sophiekoonin">@sophiekoonin</a> on <a href="https://codepen.io">CodePen</a>.</span> </p> <script async="" src="https://cpwebassets.codepen.io/assets/embed/ei.js"></script> <p>The <a href="https://www.w3.org/TR/2011/WD-html5-author-20110809/the-meter-element.html">spec</a> has a few more sensible examples, though I'm not entirely persuaded that a meter is a good illustration of newsgroup activity.</p> <p>A more tangible use case for the <code>&lt;meter&gt;</code> element would be to indicate something like available storage space, or percentage of remaining budget on a service where your plan only allows you a certain number of events or entities: an example of this would be <a href="https://sentry.io/welcome/">Sentry</a>, where your plan has a limit to the number of events/errors it'll accept, depending on how much money you throw at them.</p> <p>The key UX thing here, though, is that if it's important information it should be accompanied by a numerical value. A meter is good for an at-a-glance sense of how much of something has been used, but you need to present it alongside the actual value for it to be at all useful.</p> <figure><picture><source type="image/webp" srcset="https://localghost.dev/img/KDb8uKcNGW-280.webp 280w, https://localghost.dev/img/KDb8uKcNGW-412.webp 412w" sizes="auto"><img loading="lazy" decoding="async" src="https://localghost.dev/img/KDb8uKcNGW-280.jpeg" alt="A screenshot from Google Drive with a cloud icon next to text that says 'Storage 74% full', a graphical meter that is approx 74% full, then underneath the text '11.2 GB of 15 GB used'." width="412" height="148" srcset="https://localghost.dev/img/KDb8uKcNGW-280.jpeg 280w, https://localghost.dev/img/KDb8uKcNGW-412.jpeg 412w" sizes="auto"></picture></figure> <p>I checked both Dropbox and Google Drive, and both of them have a meter accompanied by a numerical description of how much space I've used; in both cases those meters are, of course, <code>&lt;div&gt;</code>s. Usually I'd complain about using a <code>&lt;div&gt;</code> when there's a semantic element available, but... it's not immediately clear to me what the advantage of using a <code>&lt;meter&gt;</code> would be from an accessibility viewpoint, if you've got the written description right there.</p> <p>When you're choosing the right element for the job, it's entirely possible to go too far the other way, and <em>overuse</em> semantic elements when actually they hinder more than they help.</p> <p>As my good pal and accessibility specialist Helen put it:</p> <blockquote> <p>It’s a really good example of thinking about what you’re trying to communicate and to who and whether your “semantic” choices actually enable that.</p> </blockquote> Remembering the early 00s teen website scene 2023-12-30T00:00:00Z https://localghost.dev/blog/remembering-the-early-00s-teen-website-scene/ <p>localghost.dev has a new theme! In search of a little project over the merrineum that didn’t require me to learn anything and therefore use my brain, I remembered there was a stylesheet hidden in the themes directory of my website that I hadn’t finished. The theme: teenage personal websites in the early 00s. It was a lot of fun to build, and really nostalgic to recreate the websites of my youth. Think impossibly tiny fonts, blocky layouts with a sidebar full of assorted crap, and grungey photoshop brushes. (But this time with CONTAINER QUERIES.)</p> <figure><picture><source type="image/webp" srcset="https://localghost.dev/img/6bG3Pauzhh-280.webp 280w, https://localghost.dev/img/6bG3Pauzhh-640.webp 640w, https://localghost.dev/img/6bG3Pauzhh-960.webp 960w" sizes="auto"><img loading="lazy" decoding="async" src="https://localghost.dev/img/6bG3Pauzhh-280.jpeg" alt="A screenshot of my new theme, with a purple background and two boxes - sidebar and content - scrunched up against the left hand side of the page. The font is very small. The header image has assorted grungy patterns on it with a distorted cursive font that says 'localghost'." width="960" height="531" srcset="https://localghost.dev/img/6bG3Pauzhh-280.jpeg 280w, https://localghost.dev/img/6bG3Pauzhh-640.jpeg 640w, https://localghost.dev/img/6bG3Pauzhh-960.jpeg 960w" sizes="auto"></picture></figure> <p>Try out the new theme by clicking the scribbly heart in the middle of the theme switcher.</p> <p>I touched on this era of my online past in my talk <a href="https://www.youtube.com/watch?v=vGYm9VdfJ8s">“This talk is under construction: a love letter to the personal website”</a> from FFConf 2022. <a href="https://medium.com/@ohhoe/keep-the-internet-weird-1137eece27c4">Rachel White</a> also wrote an excellent post about this topic back in 2016 called <em>Keep The Internet Weird</em>, and even gave a talk about it at <a href="https://www.youtube.com/watch?v=vji_6ofE5Wg">JSConf EU 2017</a>. This quote from her article particularly rings true for me:</p> <blockquote> <p>I can still remember spending hours on the Internet scouring free resources created by other teenagers for really great grunge brushes to create the best layouts that represented me at that point in my life.</p> </blockquote> <figure><picture><source type="image/webp" srcset="https://localghost.dev/img/W0j2rPg5wJ-280.webp 280w, https://localghost.dev/img/W0j2rPg5wJ-640.webp 640w, https://localghost.dev/img/W0j2rPg5wJ-960.webp 960w" sizes="auto"><img loading="lazy" decoding="async" src="https://localghost.dev/img/W0j2rPg5wJ-280.jpeg" alt="A screenshot of a website from 2003, with a dark red background and small pink frame in the centre of the page containing a blog. The text is very small and the content scrolls in its tiny box. The background has pink paint splatters on. The left hand sidebar has a navigation with 'girl', 'site', 'misc', 'links', 'gbook', and next to it the sentence 'girl, 17, new york, freshman in college'." width="960" height="603" srcset="https://localghost.dev/img/W0j2rPg5wJ-280.jpeg 280w, https://localghost.dev/img/W0j2rPg5wJ-640.jpeg 640w, https://localghost.dev/img/W0j2rPg5wJ-960.jpeg 960w" sizes="auto"></picture><figcaption>One of Rachel's sites, circa 2003</figcaption></figure> <p>I've recreated some of this magic in the new theme, complete with the stupidly tiny content box that scrolls, and the font that you have to squint to read.</p> <p>All of this has me reminisicing about my adventures building websites as part of this early 00s scene, so I thought I'd share some of those memories.</p> <h3 id="tld-is-everything" tabindex="-1">TLD is everything</h3> <p>If you were going to have a domain name, you had to have the right TLD. For some reason <code>.org</code> and <code>.nu</code> domain names were really fashionable. I remember sites with names like <code>partly-cloudy.org</code> and <code>lipsofpink.org</code>.</p> <p>Of course I never had a domain because my parents wouldn’t pay for it (very reasonably), so I had to do the next best thing: be hosted on someone else's website who was cool enough to have a domain.</p> <p>Since the webmasters of the early 00s let these domain names lapse a long time ago, there’s very little evidence of this scene on the internet. Google searches for “2000s teenage blog scene” come up with a lot of “Y2K” inspired tumblrs and reminiscences of the actual scene subculture, something entirely different. There’s the <a href="https://www.reddit.com/r/CasualConversation/comments/vr01wv/anyone_remember_angsty_teen_web_journals_from_the/">occasional Reddit post</a> where someone asks “hey, does anyone remember...” and there will be one person who does, and everyone else just remembers LiveJournal (everyone remembers LJ).</p> <p>Thank goodness for the Internet Archive and its Wayback Machine, which has snapshots of many of these sites. Sadly a lot of the images aren't there, so you can't experience the full, over-the-top glory of the layouts. Hopefully the new theme on this site will give you a taste of what it was like.</p> <p>I had three different hosts over a few years, and slices of those sites are still findable on the Wayback Machine, so you can be assured I won't be telling you what those domains were in the interests of self-preservation (translation: it's too embarrassing).</p> <p>It's been a stark reminder of the importance of digital archiving, finding all of these sites incomplete in some way: content missing, stylesheet missing, images missing. With every site, there’s probably going to be a point where you don’t want to renew the domain name any more, or the host itself disappears. It’s really important that you have some kind of backup of the content, not only in text format but visually as well: the Wayback Machine has only captured snippets of my old sites, and often the images and stylesheets don’t work. I’d give anything to see full screenshots of what some of those sites looked like.</p> <h3 id="this-is-my-space-but-here-s-something-for-you" tabindex="-1">This is my space, but here's something for you</h3> <p>Blog posts – annotated with the music you were listening to while writing, of course – would be little windows into your everyday life. I remember reading some older teenagers' blogs, especially those in the States, and thinking how extremely cool they seemed. Driving! Relationships! Life that didn't revolve around exams!</p> <p>The homepage would always be the blog (splash screens were passé by this point), but there would always be a page about the site's owner, then a page of things for the visitor such as downloadable graphics, Photoshop brushes or HTML snippets to use on your own site.</p> <p>Something that was particularly great about this scene was that there was a real sense of community, both literally with the networks you formed with other bloggers, but also through helping other people be a part of it by teaching them HTML skills. We'd all make friends and comment on each other's blogs and link to our friends' and fellow hostees' sites, most of whom were people that we'd never even met.</p> <p>Most importantly, though: a lot of these websites were run by teenage girls and young women. It was a space on the internet for marginalised people where we could be ourselves, whether under a real name or an alias. Our families wouldn't find it, and IRL friends wouldn't either unless you gave them the URL of your site, so the like-minded audience that you had was very small and controlled as well. It felt like a really safe space to be yourself. None of the carefully curated content feeds that you get on social media these days (or even blogs these days, a lot of the time).</p> <p>At the time, I was pretty unpopular at school, in part because I was such a computer geek (it was very much Not A Cool Hobby) and I used to spend breaks at school working on my website in the IT room. The group of friends I fell into – who I'm still very close with today – were all like-minded geeky types with our own websites. Seeing and interacting with these older teenagers online with these fancy domains and cool layouts made me feel like I was part of something and that my interests were actually socially acceptable.</p> <p>Of course, joke's on all the people who thought I was uncool, because now I'm so cool that I have a domain name that I need to explain to everyone who isn't a developer every time I give someone my email address.</p> <h3 id="version-17" tabindex="-1">Version 17</h3> <p>I’d happily fire up FileZilla and FTP into my server, set up Greymatter or later Wordpress, and spend hours after school building layouts for my website, and my friends’ websites. Later I learned some very basic PHP so that I could include layout headers and footers on separate pages of my website, and even make the footer copyright date dynamic instead of just hard-coded.</p> <p>We’d change the layouts on our sites very frequently – once a month or even more often – and keep track of which version we were on in our site sidebar. Layouts were usually a main bit and a sidebar, first with tables and later with absolutely positioned divs; every layout featured a large header image composed of a transparent cutout PSD of some band or celebrity with the obligatory grunge photoshop brushes on top. I have a handful of my later header images saved, but most are lost to time.</p> <p>There were websites offering cutouts of celebrities from magazine scans and photoshoots; I’d make header images in my cracked version of Paint Shop Pro with <a href="https://web.archive.org/web/20050130021007/http://www.lime-light.org/psds/">whatever PSDs I could get my hands on</a>, regardless of how much I actually liked that celebrity. That’s how I ended up with layouts featuring such gems as, er, Paris Hilton or Gwyneth Paltrow.</p> <figure><picture><source type="image/webp" srcset="https://localghost.dev/img/IFf9OvURpy-280.webp 280w, https://localghost.dev/img/IFf9OvURpy-640.webp 640w" sizes="auto"><img loading="lazy" decoding="async" src="https://localghost.dev/img/IFf9OvURpy-280.jpeg" alt="The header image from an old layout of mine featuring three cutouts of Gwyneth Paltrow, all greyscale with grungy textures on top, and a light green flowery pixel art background." width="640" height="480" srcset="https://localghost.dev/img/IFf9OvURpy-280.jpeg 280w, https://localghost.dev/img/IFf9OvURpy-640.jpeg 640w" sizes="auto"></picture></figure> <p>Over time, tiny scrunched-up scrolling box layouts were replaced by full-page ones, still clamped to the left hand side of the browser, but with even more room in the sidebar for fun things.</p> <figure><picture><source type="image/webp" srcset="https://localghost.dev/img/1a_uJW355A-280.webp 280w, https://localghost.dev/img/1a_uJW355A-640.webp 640w, https://localghost.dev/img/1a_uJW355A-960.webp 960w" sizes="auto"><img loading="lazy" decoding="async" src="https://localghost.dev/img/1a_uJW355A-280.jpeg" alt="A screenshot of a full page site with a grungy graphic of Tegan and Sara in the top left with lyrics from 'I Bet It Stung', and lots of very small text in both the sidebar on the right and the content underneath the header image." width="960" height="705" srcset="https://localghost.dev/img/1a_uJW355A-280.jpeg 280w, https://localghost.dev/img/1a_uJW355A-640.jpeg 640w, https://localghost.dev/img/1a_uJW355A-960.jpeg 960w" sizes="auto"></picture></figure> <h3 id="website-tsatskes" tabindex="-1">Website tsatskes</h3> <p>Another thing I miss from early websites is the random tsatskes you'd accumulate from around the web and display proudly on your homepage, and these websites were no exception. You might have <a href="https://localghost.dev/blog/remembering-the-early-00s-teen-website-scene/www.thefanlistings.org">fanlistings</a> for things you love, links to <a href="https://www.wired.com/2003/07/web-cliques-too-cool-for-school/">cliques</a> you’d joined, or 88 x 31px buttons linking to other websites.</p> <p>Those of us on PHP-enabled servers might have some <a href="https://web.archive.org/web/20040805055326/http://www.codegrrl.com/archives/cat_scripts.php">codegrrl</a> scripts like <a href="https://web.archive.org/web/20040805055326/http://www.codegrrl.com/archives/000100.php">PHPCurrently</a>, a customisable list of what you were doing at any given point in time. I mentioned this in my post about <a href="https://localghost.dev/blog/everything-should-have-an-api-adventures-in-trying-to-automate-stuff/">APIs for things</a> and I can only apologise for this absolutely awful example of what I was like as an Evanescence-obsessed teenager in 2004:</p> <figure> <picture><source type="image/webp" srcset="https://localghost.dev/img/zLFP9V2JuF-280.webp 280w, https://localghost.dev/img/zLFP9V2JuF-601.webp 601w" sizes="auto"><img loading="lazy" decoding="async" src="https://localghost.dev/img/zLFP9V2JuF-280.jpeg" class="small" alt="A screenshot from an old website. It's a list of statistics about what I'm doing. It says 'Currently: MSN display picture: Amy in a pink dress, with lyrics from Missing. date: 11th June. thinking: no more exams!!!! wearing: bathrobe. makeup: none. jewellery: none. hair: loose. MSN screenname: Grammar Nazi. time: 11:30. feeling: amused. eating: raisin wheats. drinking: nothing. surfing: this thread on AGF. you may need this (link) for some of it. IMing: no-one. hating: spelling, grammar and punctuation ignorance. Powered by PHPCurrently." width="601" height="946" srcset="https://localghost.dev/img/zLFP9V2JuF-280.jpeg 280w, https://localghost.dev/img/zLFP9V2JuF-601.jpeg 601w" sizes="auto"></picture> </figure> <p>Or maybe PHPCalendar with mundane events from your life:</p> <figure><picture><source type="image/webp" srcset="https://localghost.dev/img/4VzCCbw-5A-280.webp 280w, https://localghost.dev/img/4VzCCbw-5A-594.webp 594w" sizes="auto"><img loading="lazy" decoding="async" src="https://localghost.dev/img/4VzCCbw-5A-280.jpeg" alt="Upcoming events: 07/06 - Maths and Citizenship exams 08/06 - Physics and Geog 2 exams 09/06 - English Lit exam 10/06 - German and RS exams Powered by PHPCalendar" class="small" width="594" height="314" srcset="https://localghost.dev/img/4VzCCbw-5A-280.jpeg 280w, https://localghost.dev/img/4VzCCbw-5A-594.jpeg 594w" sizes="auto"></picture></figure> <h3 id="the-end-of-an-era" tabindex="-1">The end of an era</h3> <p>I think it was ultimately LiveJournal that killed off this scene – well, that and Myspace, probably. I moved to LJ in about 2005, and was already a late adopter. It offered a customisable blog linked to infinite user-run communities, so you didn’t have to build your own network of friends, and ultimately the experience was very similar and didn't require paying for a domain. All your friends’ posts appeared on your Friends Page, so you didn’t need RSS and you could comment on everything right there.</p> <p>Of course, websites don't really carry the same weight as they did back then. The way we experience the internet is very different, not only for those of us who have <em>seen</em> it change, but even more so for the younger generations who've grown up with endless short form video. Perhaps the current Y2K aesthetic trend will lead teens to rediscover the joy of building blogs and learning web dev skills as they trace the digital footsteps of us crusty millennials.</p> <p>🎵 <em>avril lavigne - don't tell me</em></p> A good podcast: Twenty Thousand Hertz 2023-12-24T00:00:00Z https://localghost.dev/blog/a-good-podcast-twenty-thousand-hertz/ <p>Everything you've ever wanted to know about sound, and things you didn't know you wanted to know but are glad you learned, such as:</p> <ul> <li>why all film trailers sound the same with that <a href="https://www.20k.org/episodes/boojstrikesback">deep &quot;booj&quot; noise</a></li> <li>how <a href="https://www.20k.org/episodes/spaceaudity">NASA ran live audio from the Moon</a></li> <li>how the <a href="https://www.20k.org/episodes/sulsul">language in The Sims</a> came about, and the people who voice it</li> <li>the history of <a href="https://www.20k.org/episodes/tadaitswindows">Windows startup sounds</a></li> <li>the <a href="https://www.20k.org/episodes/loudnesswars">Loudness Wars</a>, aka the reason so much popular music from the last few decades is just badly mastered</li> </ul> <p>An additional treat is the Mystery Sound competition they run every episode, where they play a snippet of an obscure (or sometimes very familiar) sound, and listeners can submit their guesses. Everything from the <a href="https://www.youtube.com/watch?v=q_slAJmZBeQ">Gracie Films tag</a> at the end of the Simpsons, to <a href="https://www.youtube.com/watch?v=tGzxq5qopoc">herring farts</a>. Every year they do a Mystery Sound Game Show episode, and <a href="https://www.20k.org/episodes/tournamentofchampions">this year's</a> is hilarious. There are some sounds in there that will no doubt trigger Pavlovian reactions for anyone listening.</p> 2023: The year in lists 2023-12-21T00:00:00Z https://localghost.dev/blog/2023-the-year-in-lists/ <p>Last year's roundup post was good fun, so I decided I'd do another one this year! I feel like I blinked in April and suddenly it was December.</p> <p>Skip to bits you care about:</p> <ul> <li><a href="https://localghost.dev/blog/2023-the-year-in-lists/#the-year-in">The year in...</a> <ul> <li><a href="https://localghost.dev/blog/2023-the-year-in-lists/#conferences">...conferences</a></li> <li><a href="https://localghost.dev/blog/2023-the-year-in-lists/#travel">...travel</a></li> <li><a href="https://localghost.dev/blog/2023-the-year-in-lists/#books">...books</a></li> <li><a href="https://localghost.dev/blog/2023-the-year-in-lists/#podcasts">...podcasts</a></li> <li><a href="https://localghost.dev/blog/2023-the-year-in-lists/#music">...music</a></li> <li><a href="https://localghost.dev/blog/2023-the-year-in-lists/#video-games">...video games</a></li> <li><a href="https://localghost.dev/blog/2023-the-year-in-lists/#learning-things">...learning things</a></li> <li><a href="https://localghost.dev/blog/2023-the-year-in-lists/#purchases">...purchases</a></li> <li><a href="https://localghost.dev/blog/2023-the-year-in-lists/#apps-and-programs">...apps and programs</a></li> <li><a href="https://localghost.dev/blog/2023-the-year-in-lists/#blog-posts">...blog posts</a></li> <li><a href="https://localghost.dev/blog/2023-the-year-in-lists/#the-web">...the web</a></li> </ul> </li> </ul> <h2 id="the-year-in" tabindex="-1">The year in...</h2> <h3 id="conferences" tabindex="-1">...conferences</h3> <p>After a bumper conference year in 2022, I decided to rein it in a bit and did four conferences this year. Four is a good number and a lot more doable than seven. I also attended a few on top of that.</p> <ul> <li>In <strong>April</strong>, I spoke about personal websites at the incredible <a href="https://beyondtellerand.com">Beyond Tellerand</a> in Düsseldorf. I also wrote about the experience. In fact, I loved it so much, I'm speaking at BTConf Berlin in 2024!</li> <li>In <strong>May</strong>, I went back to <a href="https://heypresents.com">All Day Hey</a> in Leeds. I particularly enjoyed Jack Franklin's talk on <a href="https://heypresents.com/talks/abstractions-complexities-and-off-ramps">Abstractions, complexities and off-ramps</a>.</li> <li>In <strong>June</strong> I spent my birthday at <a href="https://www.youtube.com/watch?v=H2Ux0hGQcs4">CSS Day</a> in Amsterdam where I gave my personal website talk and got completely blown away by all the new features in CSS. Later in the month I attended <a href="https://leaddev.com/staffplus-london">StaffPlus</a> at the Barbican, a very different experience from the usual kinds of conferences I tend to find myself at, but super useful for things I actually do day-to-day.</li> <li>In <strong>July</strong> I found myself in Middlesbrough, in the North East, for <a href="https://www.middlesbroughfe.co.uk/">Middlesbrough Frontend</a> – where I gave the final form of my Virtual Piano talk (refined and perfected!) and witnessed the absolute joy of a local, supportive community. I also gained a pair of <a href="https://en.wikipedia.org/wiki/Parmo">parmo socks</a>.</li> <li>At the end of <strong>August</strong> I travelled to Zurich for the first time for <a href="https://frontconference.com">Front Conference</a>, which was an absolute joy; onstage I built a website like it's 1999 wearing some very bright orange trousers, and experimented with sort-of live coding using <a href="https://inspirejs.org/">inspire.js</a>. Also a joy: travelling around the lake on a boat in the sunshine.</li> <li>Back at the Barbican in <strong>October</strong>, for the glorious <a href="https://2023.stateofthebrowser.com/">State of the Browser 2023</a>. This year, Dave made everyone's conference badges out of floppy disks (you're a legend Dave), and I got a special red one as the backup MC in case he was ill.</li> <li>It's not a conference season without going down to Brighton for <a href="https://ffconf.org">FFConf</a> in <em>November</em>, and it was excellent as always &lt;3</li> </ul> <p>Attending lots of front-of-the-frontend conferences always makes me a little sad that I don't do much of that for a living – my job is a lot more around back-of-the-frontend, leading teams and building at scale. There's been a lot of talk this year about how React and other frameworks have ruined frontend development, and I don't disagree; but it's also something I use daily at work. The best thing I can do is make sure we're using it in a sensible way and defaulting to native browser capabilities where we can. (I've got a post in me somewhere about all of this.)</p> <h3 id="travel" tabindex="-1">...travel</h3> <p>My holidays this year took me to Copenhagen with my mum, then later to Norway to visit family. We flew up to Ålesund and drove to Geiranger (our suitcase only made it as far as Oslo, but thankfully joined us the next day) and then flew back down to Oslo for a few days on my cousin's farm. It was wonderful to meet some of my family for the first time!</p> <figure> <a href="https://localghost.dev/img/blog/2023-recap/geiranger.JPG"> <picture><source type="image/webp" srcset="https://localghost.dev/img/w3o1OBSPZm-280.webp 280w, https://localghost.dev/img/w3o1OBSPZm-640.webp 640w, https://localghost.dev/img/w3o1OBSPZm-960.webp 960w" sizes="auto"><img loading="lazy" decoding="async" src="https://localghost.dev/img/w3o1OBSPZm-280.jpeg" alt="A view from the harbour of Geiranger in Norway, looking out over shimmering waters into the fjord." width="960" height="720" srcset="https://localghost.dev/img/w3o1OBSPZm-280.jpeg 280w, https://localghost.dev/img/w3o1OBSPZm-640.jpeg 640w, https://localghost.dev/img/w3o1OBSPZm-960.jpeg 960w" sizes="auto"></picture> </a> <figcaption>Geiranger </figcaption></figure> <p>My annual lads' trip with some of my best friends was in the Cotswolds this year, where we stayed in a gorgeous farmhouse and went to WWT Slimbridge to gawp at some excellent waders. We saw a rare goose that had got blown off course and across the Atlantic during its seasonal migration across North America, but it seemed to be happy enough amongst the local geese.</p> <figure> <a href="https://localghost.dev/img/blog/2023-recap/slimbridge.JPG"> <picture><source type="image/webp" srcset="https://localghost.dev/img/pqX0YhC739-280.webp 280w, https://localghost.dev/img/pqX0YhC739-640.webp 640w, https://localghost.dev/img/pqX0YhC739-960.webp 960w" sizes="auto"><img loading="lazy" decoding="async" src="https://localghost.dev/img/pqX0YhC739-280.jpeg" alt="A landscape photo of WWT Slimbridge wetlands. It's bright and sunny with a lovely view over fields and the River Severn estuary, with Wales visible on the other side." width="960" height="720" srcset="https://localghost.dev/img/pqX0YhC739-280.jpeg 280w, https://localghost.dev/img/pqX0YhC739-640.jpeg 640w, https://localghost.dev/img/pqX0YhC739-960.jpeg 960w" sizes="auto"></picture> </a> <figcaption>WWT Slimbridge, looking out over the Severn estuary towards Wales </figcaption></figure> <figure> <a href="https://localghost.dev/img/blog/2023-recap/slimbridge2.JPG"> <picture><source type="image/webp" srcset="https://localghost.dev/img/N31XKhsYt5-280.webp 280w, https://localghost.dev/img/N31XKhsYt5-640.webp 640w, https://localghost.dev/img/N31XKhsYt5-960.webp 960w" sizes="auto"><img loading="lazy" decoding="async" src="https://localghost.dev/img/N31XKhsYt5-280.jpeg" alt="A variety of ducks sit on sunny, green wetlands" width="960" height="720" srcset="https://localghost.dev/img/N31XKhsYt5-280.jpeg 280w, https://localghost.dev/img/N31XKhsYt5-640.jpeg 640w, https://localghost.dev/img/N31XKhsYt5-960.jpeg 960w" sizes="auto"></picture> </a> <figcaption>Excellent duck action at Slimbridge </figcaption></figure> <h3 id="books" tabindex="-1">...books</h3> <p>I continued to make extensive use of my library's ebook service. I tend to only read in bed, which means I often manage about 3 pages before conking out.</p> <ul> <li><a href="https://uk.bookshop.org/p/books/tomorrow-and-tomorrow-and-tomorrow-discover-the-moving-powerful-sunday-times-bestseller-that-everyone-is-talking-about-gabrielle-zevin/7312831?ean=9781529115543">Tomorrow, and Tomorrow, and Tomorrow</a>: there probably isn't anyone left who hasn't read this book, but it's really excellent. A story of two children who grow up to build video games together.</li> <li><a href="https://uk.bookshop.org/p/books/feet-of-clay-discworld-novel-19-terry-pratchett/455317?ean=9780552167574">Feet of Clay - Terry Pratchett</a>: continuing my Pratchett education with the City Watch series, this one about murders and golems.</li> <li><a href="https://uk.bookshop.org/p/books/there-there-tommy-orange/482024?ean=9781784707972">There There - Tommy Orange</a>: a stark tale about a community of Native Americans in California, the legacy of oppression and colonialism.</li> <li><a href="https://uk.bookshop.org/p/books/raven-black-ann-cleeves/849472?ean=9781529050189">Raven Black - Anne Cleeves</a>: lighter reading perhaps, but not too light, there's plenty of murder; the first of the <em>Shetland</em> series that inspired one of my favourite BBC dramas.</li> <li><a href="https://uk.bookshop.org/p/books/my-year-of-rest-and-relaxation-ottessa-moshfegh/978286?ean=9781784877477">My Year of Rest and Relaxation - Ottessa Moshfegh</a>: a very depressed woman tries to hide from the world and sleep for an entire year.</li> <li><a href="https://uk.bookshop.org/p/books/mr-penumbra-s-24-hour-bookstore-robin-sloan/2649325?ean=9781782391210">Mr Penumbra's 24-hour Bookstore - Robin Sloan</a>: An unemployed software developer gets a job at a strange bookshop and discovers a secret society hidden within.</li> <li><a href="https://uk.bookshop.org/p/books/a-woman-sibilla-aleramo/2981507?ean=9780241345726">A Woman - Sibilla Aleramo</a>: from 1906, an autobiographical novel of the author's troubled childhood growing up in Italy, her unhappy marriage to her abuser, and her decision to leave.</li> <li><a href="https://uk.bookshop.org/p/books/soul-music-discworld-the-death-collection-terry-pratchett/4603633?ean=9781473200128">Soul Music - Terry Pratchett</a>: more Pratchett! It's like a hug for your brain! In this one, Ankh-Morpork discovers rock music.</li> <li><a href="https://uk.bookshop.org/p/books/the-four-winds-the-number-one-bestselling-richard-judy-book-club-pick-kristin-hannah/6430754?ean=9781529054583">The Four Winds - Kristin Hannah</a>: poverty and struggles during the Great Depression and Dust Bowl in the Great Plains.</li> <li><a href="https://uk.bookshop.org/p/books/the-anomaly-the-mind-bending-thriller-that-has-sold-1-million-copies-herve-le-tellier/6588773?ean=9781405950800">The Anomaly - Hervé le Tellier</a>: I love a good sci-fi mystery. A plane inexplicably duplicates itself during a storm, and every passenger now has an exact double.</li> <li><a href="https://uk.bookshop.org/p/books/making-money-terry-pratchett/6928436?ean=9781804990476">Making Money - Terry Pratchett</a>: the second Moist von Lipwig/Ankh-Morpork industrialisation book. Sadly I didn't make it all the way through the third one.</li> <li><a href="https://uk.bookshop.org/p/books/sea-of-tranquility-the-instant-sunday-times-bestseller-from-the-author-of-station-eleven-emily-st-john-mandel/6247165?ean=9781529083514">Sea of Tranquility - Emily St.John Mandel</a>: from the author of Station Eleven, a sci-fi story about a rift in time with an unravelling mystery. The ending was rather abrupt, but the story itself was great.</li> <li>I also read another book of hers called <a href="https://uk.bookshop.org/p/books/the-glass-hotel-emily-st-john-mandel/4863171?ean=9781509882830">The Glass Hotel</a>, about the aftermath of a Ponzi scheme.</li> <li><a href="https://uk.bookshop.org/p/books/the-bandit-queens-parini-shroff/7351504?ean=9781838957148">The Bandit Queens - Parini Shroff</a>: a widow in an Indian village helps another woman kill her abusive husband and Events ensue. It's dark but also really charming and quite funny.</li> </ul> <h3 id="podcasts" tabindex="-1">...podcasts</h3> <p>New to me in 2023:</p> <ul> <li><a href="https://pod.link/1651876897">If Books Could Kill</a>: Michael Hobbes and Peter Shamshiri take us through some of the biggest &quot;self-help&quot;/pseudoscience books from the last few decades and deservedly tear them apart. I can't recommend this enough.</li> <li><a href="https://www.dungeonsanddaddies.com/">Dungeons and Daddies</a>: a Dungeons and Dragons-but-also-not podcast. I'm working my way through season one, which is about &quot;four dads from our world flung into a land of high fantasy and magic in a quest to rescue their lost sons&quot;. It's one of the funniest things I've ever listened to and I'm completely hooked.</li> </ul> <h3 id="music" tabindex="-1">...music</h3> <p>According to my Apple Music Replay (aka Spotify Wrapped ripoff) I actually didn't listen to a lot of music this year. It checks out, as I've been mainlining podcasts on my commute, and I don't listen to much while I work apart from the odd bit of lo-fi. Some good things came out this year, though.</p> <ul> <li><a href="https://music.apple.com/us/album/the-loveliest-time/1697646383">Carly Rae Jepsen - The Loveliest Time</a>: CRJ has a tradition of releasing a companion album for every album she releases, and this is a great one. Standout tracks include the Daft Punk-esque <em>Psychedelic Switch</em>, and <em>Kamikaze</em>.</li> <li><a href="https://music.apple.com/us/album/speak-now-taylors-version/1690839749">Speak Now - Taylor's Version</a>: I was honestly surprised how much I liked this album, given that Speak Now was always my second least favourite TS album (least favourite is the self-titled one). The maturity of her voice suits the songs so well, and I love the one she did with Hayley Williams (<em>Castles Crumbling</em>). <em>Haunted</em> is one of my favourite Taylor Swift songs, as well, so it was great to hear the re-record. I had high hopes for the 1989 re-record because that was always my favourite, but I think I actually liked this one more.</li> <li><a href="https://music.apple.com/us/album/guts/1694386825">GUTS - Olivia Rodrigo</a>: This would have completely passed me by had I not listened to an episode of Switched On Pop where they talked about two tracks from this album, <em>vampire</em> and <em>bad idea right?</em>. Boy, are these fucking great pop songs, and I can't resist a great pop song.</li> <li><a href="https://evanescence.craftrecordings.co.uk/">Evanescence - Fallen 20th Anniversary Edition</a> – after spending years embarrassed that I was really into Evanescence as a teenager, I've come out the other side and you know what, this album slaps. It's just excellent. Now I'm at the age where 20th anniversary editions start coming out for some of the albums that shaped my teenage years, which is utterly ridiculous, but the good news is that they sound great because a lot of the mastering was a bit trash in the early 00s.</li> <li><a href="https://music.apple.com/us/album/the-record/1666138312">the record - boygenius</a>: Julien Baker, Lucy Dacus and Phoebe Bridgers come up with some really glorious harmonies which is a sure way to get me on board. I'm not so much of a fan of sad girl music so I don't tend to listen to their solo stuff or anything but I really loved some of the tracks off this album. <em>Not Strong Enough</em> is a standout.</li> </ul> <p>Regretfully (especially after last year's glowing recommendation) I cleared any trace of Rammstein and associated acts out of my music library following some really dreadful accusations. I really enjoyed their music, but I couldn't really justify listening to it any more given how fucking creepy and problematic the singer is. The investigation was dropped due to lack of evidence, but there have been enough stories that came out to make me feel uncomfortable about the whole thing regardless.</p> <h3 id="video-games" tabindex="-1">...video games</h3> <p>Another great year for games! I also wrote up a list of my <a href="https://localghost.dev/games">favourite games</a>, some of which were new to me this year.</p> <p>The standout for me and basically everyone else was <a href="https://baldursgate3.game/">Baldur's Gate 3</a>. I'm 2/3 through my second playthrough; first run I played a Druid, second time is a Dark Urge playthrough. I thought I'd be evil but it turns out I'm incapable of making questionably moral decisions even in video games. I still got to romance Astarion, though – and it's extra fun as the Dark Urge. I've sunk so many hours into this game and yet I always discover something I missed.</p> <p>Other highlights from 2023:</p> <ul> <li><a href="https://www.focus-entmt.com/en/games/chants-of-sennaar">Chants of Sennaar</a>: It's a linguist's dream, a puzzle game where you have to decipher the languages of the civilisations around you through context and environmental clues. It's also really beautiful. One of the most unique puzzle games I've played - I actually played this with my husband, solving puzzles together, and it was very wholesome.</li> <li><a href="https://www.albawildlife.com/">Alba: A Wildlife Adventure</a>: you play a young girl visiting her grandparents on a Spanish island, who gets together with her friend to start a campaign to save the island's wildlife. It's so wholesome and the fauna is very accurate — I spent far too long running around the island looking for teals and kestrels. It's a great gateway to real life birdwatching!</li> <li><a href="https://disneydreamlightvalley.com/">Disney Dreamlight Valley</a>. Yes, really. I don't even like Disney that much, it wasn't a huge part of my life growing up and I haven't seen half of the films (not even the Lion King, which always shocks people when I say that) but the game is basically Animal Crossing with more expensive intellectual property, and it's been a lovely way to unwind. I'm currently in bed with sinusitis as I write this and I've absolutely been rinsing the game. Though it's firmly in the category of &quot;games that make you wonder why the residents of this town don't ever fucking contribute&quot;. If you have Apple Arcade, it's just become available on there (though no cross save); I have it on Switch and the framerate is not great, so other platforms are probably better.</li> <li><a href="https://www.cloudheadgames.com/pistol-whip">Pistol Whip</a>. I got a PS VR 2 for my birthday and it's a super fun rhythm game but with shooting.</li> <li><a href="https://www.playstation.com/en-us/games/horizon-call-of-the-mountain/">Horizon: Call Of The Mountain</a> (PS VR 2). I nearly didn't include this because it is bascially just a climbing simulator with a bit of combat, but it's a beautiful game, VR archery is so much fun and the environments you go through are so cool. I struggled a lot with this game at first because I'm actually terrified of heights, so I got quite anxious climbing some of the rusty metal structures hanging over massive drops!</li> <li><a href="https://www.nomanssky.com/">No Man's Sky</a>. Very much new to me, not new game – I got it on offer originally to try out with PS VR, but found I actually preferred playing it normally. I wasn't expecting to enjoy this game as much as I did, but it's a great combination of exploration, crafting and weird little space animals.</li> <li><a href="https://www.cyberpunk.net/gb/en/">Cyberpunk 2077</a> gets an honourable mention because the DLC was excellent and the new patch balanced a lot of the annoying things about the game. If you've been holding off, give it a go – this game rules.</li> </ul> <h3 id="learning-things" tabindex="-1">...learning things</h3> <p>Some of the things I learned this year:</p> <ul> <li>I did some really complex backend work, complained the whole way through, and begrudgingly learned a lot from it.</li> <li>I was thrown into a new situation at work which required a lot of disambiguation and interviewing lots of people to get enough information to solve a problem and make a decision on how to proceed. It was exhausting, but interesting, and great experience.</li> <li>I did a couple of painting and drawing courses and discovered that I really liked painting. Unfortunately I don't have a lot of space for the hobby right now, but you can be sure I bought all the things for it and they're in a drawer now.</li> <li>My work held a Security Awareness Week and ran a Capture the Flag competition which I got completely hooked on (and won!). As a fiend for puzzles, it ticked every box for me. I absolutely loved diving through the problems and trying to reverse engineer things. I'd really like to learn more about that stuff.</li> </ul> <p>I came away from the various frontend conferences this year intending to learn some cool new CSS stuff, but honestly my appetite for building things outside of work was so minimal that I just didn't get around to it. I did play with <code>:has</code> on the way home from CSS Day, though.</p> <h3 id="purchases" tabindex="-1">...purchases</h3> <p>The best things I bought this year:</p> <ul> <li>a second-hand soda stream. £40 off ebay, and I get sparkling water whenever I want.</li> <li>a moth-patterned dress from <a href="https://www.disturbia.co.uk/products/mortmoth-short-sleeve-midi-dress">Disturbia</a>. I've enjoyed indulging my ~inner~ outer goth this year.</li> <li>a <a href="https://www.meaco.com/collections/fans">Meaco</a> fan, which is wonderfully quiet and has about 954 different speeds, all of which feel like some kind of deity is blowing on your face.</li> <li><a href="https://shop.barbican.org.uk/products/cat-sims-conservatory-a5-risograph?_pos=1&amp;_sid=cacfa0abf&amp;_ss=r&amp;variant=40837127045168">two gorgeous prints</a> of the Barbican Conservatory by Cat Sims</li> <li><a href="https://www.dogecore.com/collections/unisex-t-shirts/products/haha-business-1">&quot;haha business&quot; t-shirt</a> from dogecore</li> <li><a href="https://media.social.lol/media_attachments/files/111/577/710/363/832/647/original/b840d6fce0948625.jpeg">Beautiful moon earrings</a> from my favourite jewellery designer <a href="https://etsy.com/shop/rosapietsch">Rosa Pietsch</a></li> </ul> <h3 id="apps-and-programs" tabindex="-1">...apps and programs</h3> <ul> <li><a href="https://apps.apple.com/gb/app/in-your-face-meeting-reminder/id1476964367?mt=12">In Your Face</a> means I'll never be late to a meeting again</li> <li><a href="https://apps.apple.com/gb/app/bettersnaptool/id417375580?mt=12">BetterSnapTool</a> lets you drag windows to the side of the screen and instantly snap them to half or a quarter of the desktop. For work that requires a lot of side-by-side comparison it's a godsend.</li> <li><a href="https://apps.apple.com/us/app/boop/id1518425043?mt=12">Boop</a> is a great little tool for doing a lot of common dev tasks like decoding URI-encoded text, converting CSV to JSON, and so on.</li> <li><a href="https://obsidian.md/">Obsidian</a>: I decided it was time to get organised and put all my notes and docs in one place so I'd actually know where they were. I'm hoping that over time it'll become a really useful reference for my own brain, which is a swirling vortex of smaller, swirlier vortexes.</li> </ul> <h3 id="blog-posts" tabindex="-1">...blog posts</h3> <p>I did even better than last year and wrote 10 posts, shared 2 recipes and talked about one podcast. Some of my favourite posts from this year:</p> <ul> <li><a href="https://localghost.dev/blog/engineering-progression-for-humans/">Engineering progression for humans</a></li> <li><a href="https://localghost.dev/blog/beautiful-musical-chaos/">Beautiful musical chaos</a></li> <li><a href="https://localghost.dev/blog/ai-and-the-trouble-with-inaccessible-saas/">AI and the trouble with inaccessible SaaS</a></li> </ul> <h3 id="the-web" tabindex="-1">...the web</h3> <ul> <li>Mastodon continued to grow on me. I still miss the old Twitter, and the reach I used to get on there, but I've kind of embraced the slowness of Mastodon now. I don't even check it every day, which seems like a depressing thing to be surprised about, but I was so hooked on Twitter.</li> <li>I subscribed to the <a href="https://www.garbageday.email/">Garbage Day</a> newsletter, and it's very good if you're an Internet Person.</li> <li>Robin Rendle and Matthias Ott started newsletters, and you should absolutely subscribe (either via RSS or email). Robin's is called <a href="https://robinrendle.com/the-cascade/">The Cascade</a> and it's about CSS; Matthias's is <a href="https://buttondown.email/ownyourweb">Own Your Web</a>, about designing, building and publishing on the web. They are both Very Good.</li> </ul> <h2 id="and-so-we-re-at-the-end" tabindex="-1">And so we're at the end <!-- omit in toc --></h2> <p>It's been a whirlwind year and I've ended it pretty shattered, but I'm pleased with what I achieved (and by that I mean saving the world in Baldur's Gate 3, obviously).</p> <p>I used a fair bit of my time off this year on conferences which, although fun, are not restful in the slightest. That on top of a stressful few months towards the end of the year mean I've ended it a bit burnt out again. Not as bad as last year, but still not great. The plus side: I get a 3-month paid sabbatical next year (!) so I'm going to make sure I get more of a rest. I'm thinking of doing some volunteering/conservation work with a wildlife trust or something similar, and we're hoping to finally go to Japan later in 2024.</p> <p>Wishing you all a lovely Christmas, a happy new year, and a 2024 that's exactly what you are hoping for!</p> Engineering progression for humans 2023-12-12T00:00:00Z https://localghost.dev/blog/engineering-progression-for-humans/ <p>As engineers it can be unclear where we’re heading, especially when we’re heads down writing code all the time. It can be hard to think even a few months ahead. What does an engineering <em>career</em> in a larger organisation look like? What even is a staff engineer? How long should you stay at the same company?</p> <p>Whether you're new to the industry or several years in, there's a good chance you've identified with at least one of these statements at some point:</p> <ul> <li>It seems like there’s a dozen different ways you could progress and you have no idea what’s right for you</li> <li>There isn’t anyone in your immediate organisation who has a role you see yourself going into</li> <li>You don’t really know what you want from your career</li> </ul> <p>This is everything I've learned about progression in larger organisations, with some thoughts and opinions on the way. If you're not working in a capital-T capital-C Tech Company, this might not be so applicable to your situation, but never say never!</p> <p>This <em>isn't</em> going to be a step-by-step guide to how to reach each level of the engineering career path. For one thing, it varies wildly from company to company, so my advice might not be applicable where you work. Instead, by looking at what these levels involve, you might be able to identify where you need more experience or where you're already well on your way.</p> <h3 id="know-what-motivates-you" tabindex="-1">Know what motivates you</h3> <p>Understanding how you want to progress in your engineering career depends on what motivates you.</p> <p>It’s completely okay to be motivated by money, for example. Money can be exchanged for goods and services. Houses are expensive. Families are expensive. Dogs are expensive. Maybe you’re saving for your future, or supporting someone back home, or maybe you like having nice things.</p> <p>Or maybe you want to be in a position where you can influence teams at a higher level. You have thoughts and opinions, and you like being able to help architect systems or decide on the best way forward.</p> <p>Perhaps you want a role that lets you help others, and grow people. Watching them develop in their own careers thanks to your support. Helping teams achieve their goals and be productive.</p> <p>You might need a change because your current role is negatively affecting your mental health. For me, mental health trumps everything, and there is no job that is worth doing at the expense of mental health. <a href="https://x.com/dasharez0ne/status/1125839557352742913">If it sucks, hit the bricks</a>.</p> <p>Maybe you want to do work, clock out, and focus on the other things in life. Like extreme ironing, or beekeeping.</p> <p>You’ll probably fall into a few of these categories. I know I’m definitely motivated by helping people but also taking a high-level view and influencing things. And I have to say, I also like having money. But I've also been careful to choose jobs that allow me to protect my work-life balance.</p> <p>Your career decisions should reflect what’s best for you. Not your company. Not anyone else. We spend 40 hours a week in our jobs, so it needs to be worth it.</p> <p>Speaking of which:</p> <h3 id="not-everybody-needs-to-or-wants-to-progress" tabindex="-1">Not everybody needs to (or wants to) progress</h3> <p>It's so easy to fall into the trap of thinking you have to follow the same path as everyone else. You could be forgiven for thinking careers are all about progression. But ultimately it's whatever you want it to be, and if you're happy where you are, that's the most important thing. Many folks out there will tell you that you don't have to have a Big Tech job to be a successful engineer, and you should listen to them. Engineering, software development, web design, whatever you want to call it: there are so many versions of this job and only you know which one suits you best.</p> <h3 id="the-way-up-or-sideways-or-backwards" tabindex="-1">The way up (or sideways. Or backwards.)</h3> <p>The first few stages of the engineering career path are relatively well-trodden. You come in at an entry-level or so-called “junior” position, you work your way up to a mid-level engineer – often just called “engineer” – and then you might go up to Senior. Here's one such example:</p> <figure><a href="https://localghost.dev/img/blog/eng-progression/eng-path.png"><picture><source type="image/webp" srcset="https://localghost.dev/img/6M__irvwwS-280.webp 280w, https://localghost.dev/img/6M__irvwwS-640.webp 640w, https://localghost.dev/img/6M__irvwwS-960.webp 960w" sizes="auto"><img loading="lazy" decoding="async" src="https://localghost.dev/img/6M__irvwwS-280.jpeg" alt="A graph of nodes demonstrating the first few stages of engineering progression. The first node is Engineer I, which has an arrow to Engineer II, then Engineer III, then Senior Engineer I, then Senior Engineer II." width="960" height="66" srcset="https://localghost.dev/img/6M__irvwwS-280.jpeg 280w, https://localghost.dev/img/6M__irvwwS-640.jpeg 640w, https://localghost.dev/img/6M__irvwwS-960.jpeg 960w" sizes="auto"></picture></a></figure> <p>What each of these steps looks like depends on your organisation, but commonly Engineer I is like an intern or apprentice (what you might expect for someone with little to no background in engineering at all), Engineer II is what is often called &quot;junior&quot; (entry-level for someone with a qualification or a bit of training) and Engineer III is a mid-level position. Senior might just be one role, or split out into two levels.</p> <p>It depends on where you work, but generally speaking there are no set timelines for this progression. Some folks progress faster than others, or have their start at different times in their life: sometimes you’ll work with a 22-year-old senior engineer and feel like you are turning into dust, sometimes you’ll meet folks in their 40s or 50s who are starting out as developers. It’s all good.</p> <p>A note on promotions: many tech companies these days will only consider a promotion once you've shown that you're already working at the next level up. I don't especially like this, as I think it disadvantages people who aren't extroverted or highly visible in an organisation, and it kind of means you end up doing the job for a while before you get paid for it. In these kinds of organisations, progression is as much about playing the game as it is about doing a good job and being recognised for it. I'll write up another post about that soon.</p> <p>I also know some bigger orgs have a kind of rule that you have to be in a role for X number of years before you can get promoted, and I particularly don’t agree with that approach because your tenure has no bearing on your ability. I'd agree that it takes time to build up the required experience for higher levels of jobs, but that amount of time differs from person to person and each individual case should be considered.</p> <p>The first few stages of an engineer's career are a bit more well-defined; let's have a look at them.</p> <h4 id="engineer-i-and-ii" tabindex="-1">Engineer I and II</h4> <p>As Engineer I, you're at the start of your career, learning and asking questions. Not that you won't be learning and asking questions for the rest of your career – you should definitely be doing that regardless of your experience. But you'll be getting to grips with the tools, with the practices, with how things work.</p> <p>The kind of work you do at this stage should be very well-defined tasks that are curated for you by a more senior engineer as good things to get your teeth into and learn from. You might spend a lot of time pairing on tasks.</p> <p>At level II, you've got the base knowledge down, you're a solid, trusted contributor to your team and beginning to design solutions on a smaller scale for problems your team is working on. You're more independent now, though you're probably still working with senior engineers to break down the problems and scope the work that you're doing.</p> <h4 id="mid-level-engineer-engineer-iii" tabindex="-1">Mid-level engineer / Engineer III</h4> <p>Now you're a lot more independent – a good individual contributor (IC). You're happy designing and implementing solutions, suggesting improvements to ways of working and engineering patterns, writing code with a good level of proficiency. You may also start to mentor less experienced engineers. You'll be leading mid-sized projects and are trusted to get the job done. Your PR reviews and pairing sessions help more junior engineers learn the ropes.</p> <p>It’s totally legit to just stay here and hang out for however long you want. You don’t need to go for senior.</p> <h3 id="senior-engineer" tabindex="-1">Senior engineer</h3> <p>Senior engineers are experienced ICs who can lead projects or features that have a higher level of technical complexity. You'll be mentoring less experienced engineers, either through formal 1:1s or by leading by example through the code you ship and the feedback you leave on PRs. You'll get good at identifying opportunities for your team and how they relate to the wider goals of your organisation.</p> <p>The more senior you get, the more ambiguity there is in what you’re working on, and the more high-level you start to operate. My work often finds me looking across teams and even across the organisation.</p> <h4 id="senior-engineers-come-in-all-shapes-and-sizes" tabindex="-1">Senior engineers come in all shapes and sizes!</h4> <p>The senior engineer role isn't the same for everyone. I like how <a href="https://monzo.com/documents/engineering-progression-framework-v3-0.pdf">Monzo's engineering progression framework</a>, er, frames it, and I've very much paraphrased here:</p> <p>You might be a galaxy-brained domain expert, who knows the ins and outs of a particular area or system like the back of your hand. If there's a project involving this system, you're possibly leading it, but you're <em>definitely</em> advising on it.</p> <p>Or you might be more of a &quot;tech lead&quot;-shaped senior engineer, focussing on enabling your team to build great quality software and fostering a great engineering culture. At Monzo this doesn’t translate to literally being tech lead of a team – it’s a hat you put on, and you might not be senior yet when you're tech-leading.</p> <p>You can also sit somewhere in the middle, being a great individual contributor and leading projects, helping your team to do great things.</p> <h4 id="senior-to" tabindex="-1">Senior to ???</h4> <p>What comes next?</p> <p>So, for one thing, maybe you don’t go anywhere.</p> <p>The long-tenured senior engineers on our teams are indispensable. They’re fountains of knowledge, they’ve seen it all, they are wise beyond their years. Every company needs some great senior engineers who are happy continuing to be great senior engineers. You may choose to reach senior, and stay there.</p> <h4 id="senior-to-staff" tabindex="-1">Senior to staff</h4> <p>If you want to continue down the IC path, generally it'll be to something like Staff Engineer. This is technically a leadership position, but it shouldn’t involve managing people.</p> <p>What this role looks like varies massively across different organisations and even across different teams in the same organisation.</p> <p>If you do decide to pursue further progression to Staff and above, this is where things start to get murky. It’s surprisingly not a very well-trodden path.</p> <p>Thankfully, both Tanya Reilly and Will Larson have done lots of path-treading and word-writing about this very topic: Tanya’s book, <a href="https://www.oreilly.com/library/view/the-staff-engineers/9781098118723/">The Staff Engineer’s Path</a>, provides a great overview of what you might expect to be doing and what you need to excel in this role. Will Larson is the author of <a href="https://staffeng.com/book/">Staff Engineer</a> and also runs the <a href="https://staffeng.com/">StaffEng</a> website with a lot of helpful stories and guides to staff engineering across organisations.</p> <p>Notably, he came up with <a href="https://staffeng.com/guides/staff-archetypes/">Staff Engineer archetypes</a> that nicely outline what this role might look like:</p> <blockquote> <p>The Tech Lead guides the approach and execution of a particular team. They partner closely with a single manager, but sometimes they partner with two or three managers within a focused area. Some companies also have a Tech Lead Manager role, which is similar to the Tech Lead archetype but exists on the engineering manager ladder and includes people management responsibilities.</p> <p>The Architect is responsible for the direction, quality, and approach within a critical area. They combine in-depth knowledge of technical constraints, user needs, and organization level leadership.</p> <p>The Solver digs deep into arbitrarily complex problems and finds an appropriate path forward. Some focus on a given area for long periods. Others bounce from hotspot to hotspot as guided by organizational leadership.</p> <p>The Right Hand extends an executive's attention, borrowing their scope and authority to operate particularly complex organizations. They provide additional leadership bandwidth to leaders of large-scale organizations.</p> </blockquote> <p>I’m working on this path myself, recognising the areas I need to develop. My engineering director framed it as having muscles I need to develop and train to get to staff; for example, I know I’m very technically competent and have formed good relationships across the company, but I need to get better at managing my time and focus, and I'm gaining experience of tackling highly ambiguous problems.</p> <p>This year we also had the inaugural <a href="https://leaddev.com/staffplus-london">StaffPlus</a> conference from the same folks that run LeadDev. I found this a really great opportunity to absorb some really useful information for my own progression in a bigger organisation. (Shame about the eye-watering ticket price, though.)</p> <h4 id="senior-to-engineering-management" tabindex="-1">Senior to engineering management</h4> <p>In tech companies these days it’s very common to have dedicated engineering managers (EMs) who look after a group or team of engineers, along with some other responsibilities. At Monzo our EMs also function as delivery leads, making sure the teams are unblocked and productive, and acting as envoys between product teams and leadership.</p> <p>You might take a step sideways away from the hands-on engineering role, and become an engineering manager. It’s not a promotion as such, more of a role change; generally speaking engineers wanting to make the transition into engineering management will need to get to senior first, before moving laterally.</p> <p>The most well-known guide in this area is <a href="https://www.oreilly.com/library/view/the-managers-path/9781491973882/">The Manager’s Path</a> by Camille Fournier. I read a good part of it myself some years ago when I was looking into becoming a tech lead, and it has some great advice on what makes an effective technical manager (and of course, how to get there).</p> <p>I know plenty of engineers who have gone into engineering management and loved it; I also know a handful who have done it for a while and realised it wasn't for them and gone back to being an IC. It doesn't have to be a permanent choice.</p> <h4 id="other-potential-sideways-steps" tabindex="-1">Other potential sideways steps</h4> <p>I've outlined some very common paths for engineers, but there are other roles you might choose to explore in the world of engineering that are more of a sideways step, such as:</p> <ul> <li><a href="https://medium.com/@dev.n/understanding-devrel-a-beginners-guide-a71063e4249e">Developer relations</a> (devrel) – fostering relationships with the developers who use products, showing them the art of the possible, and building a community around that</li> <li><a href="https://uk.indeed.com/career-advice/finding-a-job/what-does-sales-engineer-do">Sales engineering</a> – combining technical expertise and people skills in a role that could see you travelling all over!</li> <li>Specialist consultancy roles e.g. accessibility consultancy – laser-focussing your skills in a certain area and helping companies to improve, or even taking on a full-time leadership position as head of your specialism.</li> </ul> <p>(If you know of any others I've missed, do let me know, and I'll add them in.)</p> <p>So, that's the engineer's path. But it's rare that people spend their entire careers in one company these days. Where might your career take you?</p> <h3 id="cross-company-career-paths-illustrated-using-tenuous-playground-metaphors" tabindex="-1">Cross-company career paths illustrated using tenuous playground metaphors</h3> <p>You might recognise these paths from your own career or those of the people around you. They're somewhat simplified for brevity.</p> <h4 id="the-ladder" tabindex="-1">The ladder</h4> <figure> <picture><source type="image/webp" srcset="https://localghost.dev/img/1x1fu1USl5-280.webp 280w, https://localghost.dev/img/1x1fu1USl5-640.webp 640w, https://localghost.dev/img/1x1fu1USl5-960.webp 960w" sizes="auto"><img loading="lazy" decoding="async" src="https://localghost.dev/img/1x1fu1USl5-280.jpeg" alt="A yellow-painted metal playground ladder, viewed from the top. The steps say 'MEXICO FORGE'." width="960" height="640" srcset="https://localghost.dev/img/1x1fu1USl5-280.jpeg 280w, https://localghost.dev/img/1x1fu1USl5-640.jpeg 640w, https://localghost.dev/img/1x1fu1USl5-960.jpeg 960w" sizes="auto"></picture> <figcaption>Photo by <a href="https://unsplash.com/@mglazier98">Michael Glazier</a> on <a href="https://unsplash.com/photos/a-yellow-sign-with-black-text-LZwp8gGP74s">Unsplash</a> </figcaption></figure> <p>The “traditional” career ladder model tended to be applied to people within one organisation. At a company I used to work at, it looked a bit like this:</p> <figure><a href="https://localghost.dev/img/blog/eng-progression/ladder-2.png"><picture><source type="image/webp" srcset="https://localghost.dev/img/jKYwcWIWHN-280.webp 280w, https://localghost.dev/img/jKYwcWIWHN-640.webp 640w, https://localghost.dev/img/jKYwcWIWHN-960.webp 960w" sizes="auto"><img loading="lazy" decoding="async" src="https://localghost.dev/img/jKYwcWIWHN-280.jpeg" alt="A diagram of several nodes, each pointing to the next from left to right. The first node is labelled Company 1 Engineer, which points to Company 1 Engineer again, then Company 1 Senior Engineer, then Company 1 Senior Engineer &amp; Manager, then Company 1 Senior Manager." width="960" height="80" srcset="https://localghost.dev/img/jKYwcWIWHN-280.jpeg 280w, https://localghost.dev/img/jKYwcWIWHN-640.jpeg 640w, https://localghost.dev/img/jKYwcWIWHN-960.jpeg 960w" sizes="auto"></picture></a></figure> <p>It was a very old company, and people stayed for their whole careers. The more senior you got, the bigger the likelihood was you’d end up managing people. There wasn’t really an IC track in the way there is elsewhere.</p> <figure><a href="https://localghost.dev/img/blog/eng-progression/ladder-1.png"><picture><source type="image/webp" srcset="https://localghost.dev/img/I9vhLekdKV-280.webp 280w, https://localghost.dev/img/I9vhLekdKV-640.webp 640w, https://localghost.dev/img/I9vhLekdKV-960.webp 960w" sizes="auto"><img loading="lazy" decoding="async" src="https://localghost.dev/img/I9vhLekdKV-280.jpeg" alt="A diagram of several nodes, each pointing to the next from left to right. The first node is labelled Company 1 Engineer, which points to Company 1 Engineer again, then Company 1 Senior Engineer, then Company 1 Staff Engineer, then Company 1 Senior Staff Engineer." width="960" height="61" srcset="https://localghost.dev/img/I9vhLekdKV-280.jpeg 280w, https://localghost.dev/img/I9vhLekdKV-640.jpeg 640w, https://localghost.dev/img/I9vhLekdKV-960.jpeg 960w" sizes="auto"></picture></a></figure> <p>With a long career in one place, you get great in-depth familiarity with systems and organisational patterns, and you become a trusted face in the company. Many companies offer bonuses and benefits for long service, like sabbaticals. And if you're happy there, that's great.</p> <p>Do bear in mind that that tenure might come at a cost of not gaining experience of the outside world: you could end up with tunnel vision, only knowing how things are done where you work. As time goes by you might find yourself more resistant to change, because it’s not the way you’ve always done things (this was a common pattern where I worked). There are ways around this, of course – make sure you keep in touch with folks from other orgs, hire people from outside and trust their opinions, and keep up to date with what’s going on in the wider industry.</p> <h4 id="the-climbing-frame" tabindex="-1">The climbing frame</h4> <figure> <picture><source type="image/webp" srcset="https://localghost.dev/img/mKI3n0vnLq-280.webp 280w, https://localghost.dev/img/mKI3n0vnLq-640.webp 640w, https://localghost.dev/img/mKI3n0vnLq-960.webp 960w" sizes="auto"><img loading="lazy" decoding="async" src="https://localghost.dev/img/mKI3n0vnLq-280.jpeg" alt="An artistic photo of a climbing frame, zoomed in on the frame itself" width="960" height="640" srcset="https://localghost.dev/img/mKI3n0vnLq-280.jpeg 280w, https://localghost.dev/img/mKI3n0vnLq-640.jpeg 640w, https://localghost.dev/img/mKI3n0vnLq-960.jpeg 960w" sizes="auto"></picture> <figcaption>Photo by <a href="https://unsplash.com/@s_tsuchiya">S. Tsuchiya</a> on <a href="https://unsplash.com/photos/a-close-up-of-a-roller-coaster-tnI2qV2IF6I">Unsplash</a> </figcaption></figure> <figure><a href="https://localghost.dev/img/blog/eng-progression/climbing-frame.png"><picture><source type="image/webp" srcset="https://localghost.dev/img/yJWm4Wbb7g-280.webp 280w, https://localghost.dev/img/yJWm4Wbb7g-640.webp 640w, https://localghost.dev/img/yJWm4Wbb7g-960.webp 960w" sizes="auto"><img loading="lazy" decoding="async" src="https://localghost.dev/img/yJWm4Wbb7g-280.jpeg" alt="A diagram of career moves. We begin with Company 1 Engineer, which has an arrow pointing to Company 2 Engineer; that points to Company 2 Senior Engineer, which in turn points to Company 3 Senior Engineer." width="960" height="294" srcset="https://localghost.dev/img/yJWm4Wbb7g-280.jpeg 280w, https://localghost.dev/img/yJWm4Wbb7g-640.jpeg 640w, https://localghost.dev/img/yJWm4Wbb7g-960.jpeg 960w" sizes="auto"></picture></a></figure> <p>A more common pattern these days is to move every few years. You might take a promotion for the new role, or do a sideways step into another company at the same level. You don’t always have to be getting a promotion when you move, though 9 times out of 10 you should be getting a pay rise with that move, unless it’s a conscious decision (e.g. moving to a nonprofit or smaller company).</p> <p>This might also involve a role change – I’ve known engineers who climbed the ladder and then did a sideways step into a non-engineering role. That engineering background is so powerful when you go into product management, for example.</p> <h4 id="monkey-bars" tabindex="-1">Monkey bars</h4> <figure> <picture><source type="image/webp" srcset="https://localghost.dev/img/NTQb4E0yqm-280.webp 280w, https://localghost.dev/img/NTQb4E0yqm-640.webp 640w, https://localghost.dev/img/NTQb4E0yqm-960.webp 960w" sizes="auto"><img loading="lazy" decoding="async" src="https://localghost.dev/img/NTQb4E0yqm-280.jpeg" alt="A child's hand clutches one of several handles hanging from a horizontal wooden pole on a jungle gym" width="960" height="670" srcset="https://localghost.dev/img/NTQb4E0yqm-280.jpeg 280w, https://localghost.dev/img/NTQb4E0yqm-640.jpeg 640w, https://localghost.dev/img/NTQb4E0yqm-960.jpeg 960w" sizes="auto"></picture> <figcaption>Photo by <a href="https://unsplash.com/@anniespratt">Annie Spratt</a> on <a href="https://unsplash.com/photos/girl-playing-at-monkey-bar-during-daytime-1Jg-_nekJT0">Unsplash</a> </figcaption></figure> <figure><a href="https://localghost.dev/img/blog/eng-progression/monkey-bars.png"><picture><source type="image/webp" srcset="https://localghost.dev/img/t4pkTT3R73-280.webp 280w, https://localghost.dev/img/t4pkTT3R73-640.webp 640w, https://localghost.dev/img/t4pkTT3R73-960.webp 960w" sizes="auto"><img loading="lazy" decoding="async" src="https://localghost.dev/img/t4pkTT3R73-280.jpeg" alt="A diagram of career moves. We begin with Company 1 Engineer, which has an arrow pointing to Company 2 Engineer; that points to Company 3 Engineer, which in turn points to Company 4 Engineer, which goes to Company 5 Engineer." width="960" height="174" srcset="https://localghost.dev/img/t4pkTT3R73-280.jpeg 280w, https://localghost.dev/img/t4pkTT3R73-640.jpeg 640w, https://localghost.dev/img/t4pkTT3R73-960.jpeg 960w" sizes="auto"></picture></a></figure> <p>Some folks hop from company to company every 1-2 years; there’s nothing inherently wrong with this approach. The old advice goes that lots of short stints at different places will look bad on your CV/résumé, but 1-2 years is a decent amount of time in the industry these days.</p> <p>One advantage is that you get insight into how different companies work and different tech stacks, and you should hopefully be negotiating pay rises with those moves, even if they’re lateral moves. Some of those roles might involve promotions, as well.</p> <p>Of course, people working short-term contracts will have lots of different roles in their employment history and that’s to be expected.</p> <p>The downside is that if you’re more into the leadership side of things, you might not be hanging around long enough to establish yourself as an authority of anything, and being really familiar with your organisation and its systems. So have a think about whether that’s something you value.</p> <h4 id="merry-go-round" tabindex="-1">Merry-go-round</h4> <figure> <picture><source type="image/webp" srcset="https://localghost.dev/img/22l-ENlNLH-280.webp 280w, https://localghost.dev/img/22l-ENlNLH-640.webp 640w, https://localghost.dev/img/22l-ENlNLH-960.webp 960w" sizes="auto"><img loading="lazy" decoding="async" src="https://localghost.dev/img/22l-ENlNLH-280.jpeg" alt="A faded blue merry-go-round with red seats in the middle of that spongy playground flooring" width="960" height="641" srcset="https://localghost.dev/img/22l-ENlNLH-280.jpeg 280w, https://localghost.dev/img/22l-ENlNLH-640.jpeg 640w, https://localghost.dev/img/22l-ENlNLH-960.jpeg 960w" sizes="auto"></picture> <figcaption>Photo by <a href="https://unsplash.com/@loegunntunghok">Loegunn Lai</a> on <a href="https://unsplash.com/photos/2-black-metal-framed-red-padded-armchairs-on-brown-sand-near-body-of-water-during-daytime-SyZMtSYTPy0">Unsplash</a> </figcaption></figure> <figure><a href="https://localghost.dev/img/blog/eng-progression/merry-go-round.png"><picture><source type="image/webp" srcset="https://localghost.dev/img/5aDJ4ZicBT-280.webp 280w, https://localghost.dev/img/5aDJ4ZicBT-640.webp 640w, https://localghost.dev/img/5aDJ4ZicBT-960.webp 960w" sizes="auto"><img loading="lazy" decoding="async" src="https://localghost.dev/img/5aDJ4ZicBT-280.jpeg" alt="A diagram of career moves. We begin with Company 1 Graduate, which has an arrow pointing to Company 1 Engineer II; that points to Company 2 Engineer III, which in turn points to Company 2 Senior Engineer I. This node has two arrows coming out of it. The first points to Company 3 Senior Engineer, and then the arrow from that points back to Company 2 Senior Engineer I. The second arrow points to Company 2 Senior Engineer II." width="960" height="264" srcset="https://localghost.dev/img/5aDJ4ZicBT-280.jpeg 280w, https://localghost.dev/img/5aDJ4ZicBT-640.jpeg 640w, https://localghost.dev/img/5aDJ4ZicBT-960.jpeg 960w" sizes="auto"></picture></a></figure> <p>This is actually my career history. Also known as the <a href="https://giphy.com/gifs/fDO2Nk0ImzvvW">&quot;Grampa Simpson&quot;</a>.</p> <p>I left my job at Monzo to pursue a role I was offered at a startup because I thought it was a great opportunity I shouldn’t pass up. It ended up not working out – our expectations of the role weren’t really aligned, it was nobody's fault – so I ended up coming back to Monzo in a similar role but a different team. At Monzo they call this a “boomerang”, but that doesn’t fit with my ridiculous playground analogy.</p> <p>I don’t regret leaving to try it out, and I don’t regret coming back. It’s completely fine to make a U-turn as long as it’s the right thing for you. As I wrote at the time: <a href="https://localghost.dev/blog/when-going-back-doesn-t-mean-going-backwards/">going back doesn’t always mean going backwards</a>.</p> <p>Like with any merry-go-round, you don’t want to spin too many times or you’ll get dizzy. It’s good to go on some other equipment too.</p> <h4 id="the-rocker" tabindex="-1">The rocker</h4> <figure> <picture><source type="image/webp" srcset="https://localghost.dev/img/sqoPIvyTOF-280.webp 280w, https://localghost.dev/img/sqoPIvyTOF-640.webp 640w, https://localghost.dev/img/sqoPIvyTOF-960.webp 960w" sizes="auto"><img loading="lazy" decoding="async" src="https://localghost.dev/img/sqoPIvyTOF-280.jpeg" alt="A double-seater rocker playground toy in the shape of a helicopter, supported by a large metal spring" width="960" height="640" srcset="https://localghost.dev/img/sqoPIvyTOF-280.jpeg 280w, https://localghost.dev/img/sqoPIvyTOF-640.jpeg 640w, https://localghost.dev/img/sqoPIvyTOF-960.jpeg 960w" sizes="auto"></picture> <figcaption>Photo by <a href="https://unsplash.com/@crazeker">Shiau Ling</a> on <a href="https://unsplash.com/photos/yellow-and-black-robot-toy-wNo1WTdZ_ec">Unsplash</a> </figcaption></figure> <figure><a href="https://localghost.dev/img/blog/eng-progression/rocker.png"><picture><source type="image/webp" srcset="https://localghost.dev/img/wtPJKPI-8G-280.webp 280w, https://localghost.dev/img/wtPJKPI-8G-640.webp 640w, https://localghost.dev/img/wtPJKPI-8G-960.webp 960w" sizes="auto"><img loading="lazy" decoding="async" src="https://localghost.dev/img/wtPJKPI-8G-280.jpeg" alt="Three nodes, each with an arrow going into the next one. Company 1 Engineer points to Company 2 Engineer which points to Company 3 Engineer." width="960" height="316" srcset="https://localghost.dev/img/wtPJKPI-8G-280.jpeg 280w, https://localghost.dev/img/wtPJKPI-8G-640.jpeg 640w, https://localghost.dev/img/wtPJKPI-8G-960.jpeg 960w" sizes="auto"></picture></a></figure> <p>Not everyone wants to climb to the top, and that’s okay. There are any number of reasons why someone might want to stay in the same role.</p> <p>Maybe they like it. Maybe they’ve got other responsibilities outside of work that take up any extra space in their brain. Maybe they value other things in life. It’s all good.</p> <p>I’m definitely guilty of feeling like I always need to be moving up to the next level, as if by staying still I’m not good enough; but of course, that’s absolute rubbish.</p> <h3 id="there-s-no-one-right-way" tabindex="-1">There's no one right way</h3> <p>Progression can mean so many different things. It can mean going forwards and backwards, moving around trying different things and coming back again, or staying in the same place having a great time. It really depends what suits you. There isn't one &quot;right&quot; path to follow!</p> <p>I hope this has gone some way to demystify the path your career could take, both role-wise and company-wise. And remember, don't let anyone make you feel like you <em>must</em> pursue one particular path or fit your career into a specific box. Only you can make that decision.</p> Beautiful musical chaos 2023-12-02T00:00:00Z https://localghost.dev/blog/beautiful-musical-chaos/ <p>When I tell people I’m in a choir, some of them imagine the same thing: churches, robes and <em>choral music</em>. Pious singing faces.</p> <p>(To be clear, I’ve done that kind of choir before and it’s great in its own way, but that’s very much not what goes on in this one.)</p> <p>Generally, though, people respond with the question, “what kind of choir is it?”. What indeed.</p> <p>A <a href="https://www.hawksworx.com/blog/on-song/">post by Phil Hawksworth</a> earlier this year about his newfound love of choir really resonated because I knew exactly how it felt to be completely, unexpectedly sucked in to this hobby.</p> <figure> <picture><source type="image/webp" srcset="https://localghost.dev/img/d_QXVVaWeZ-280.webp 280w, https://localghost.dev/img/d_QXVVaWeZ-640.webp 640w" sizes="auto"><img loading="lazy" decoding="async" src="https://localghost.dev/img/d_QXVVaWeZ-280.jpeg" alt="a photo of the choir on a stage at the Union Chapel in Islington, with the iconic round flower shaped window above them" width="640" height="640" srcset="https://localghost.dev/img/d_QXVVaWeZ-280.jpeg 280w, https://localghost.dev/img/d_QXVVaWeZ-640.jpeg 640w" sizes="auto"></picture> <figcaption>Daylight Music at the Union Chapel</figcaption></figure> <p>I’d been in various choirs before, classical ones, but only joined <a href="https://shechoir.com">SHE Choir</a> because I’d just moved back to London and didn’t know anyone. The choir started life as a student society in Manchester where I studied, and I’d gone to a few sessions the year before, but now the founders had moved back to London and brought the choir with them. It was chaotic, but I was hooked immediately.</p> <p>A few months in, I decided I wanted to try arranging something. I remember asking if it was okay for me to have a go, and I put together a three-part arrangement of Don’t Panic by Coldplay. I was too nervous to sing in front of people so I got someone else to teach it. But it sounded <em>good</em>.</p> <p>A weekend away with both the London and Manchester choirs to a druid camp in a remote part of the Midlands – involving sleeping next to a creepy fur-clad mannequin, lots of singing in yurts, and a bunch of city women completely failing to work a log fire – finally gave me the confidence to start singing on my own in front of people. I realised that choir was a safe space for me to do that, to teach and learn at the same time. After I got back, coasting on the high from the weekend, I arranged Fleetwood Mac’s song Everywhere and that would become the first thing I taught by myself.</p> <p>From there, the choir became something I could actively contribute to. Not long after that the founders left and, not wanting to see this lovely motley crew disappear, a couple of us took over as organisers. That was a lot of work for two people (you can see why people do it as a full-time job!) so we eventually transitioned to a team-based structure. It’s less rigorous and a bit more anarchic, but it means less responsibility falls on one person’s shoulders. (I’m always and forever the admin queen... and of course I built the website.)</p> <figure><picture><source type="image/webp" srcset="https://localghost.dev/img/lKsc7f58pP-280.webp 280w, https://localghost.dev/img/lKsc7f58pP-640.webp 640w, https://localghost.dev/img/lKsc7f58pP-960.webp 960w" sizes="auto"><img loading="lazy" decoding="async" src="https://localghost.dev/img/lKsc7f58pP-280.jpeg" alt="Sophie leans against a desk, hand in the air to indicate changes in pitch, mouth in an ooh shape as she teaches a song. There are two seated people visible and more out of shot." width="960" height="960" srcset="https://localghost.dev/img/lKsc7f58pP-280.jpeg 280w, https://localghost.dev/img/lKsc7f58pP-640.jpeg 640w, https://localghost.dev/img/lKsc7f58pP-960.jpeg 960w" sizes="auto"></picture></figure> <p>We became an a cappella group solely because none of us could play the piano. I got us on Slack because that was what you did in 2014, and we got organised. We started taking attendance, and the revolving door of people coming and going and never learning the songs became a more reliable core membership of regulars who got better every week. I got fed up of taking attendance very quickly, and built a <a href="https://github.com/sophiekoonin/shebot">Slack integration</a> that did it for us.</p> <p>We met up with another more established women’s choir in London and attended each other’s rehearsals to swap tips. They had been the inspiration for our choir’s existence. I was blown away by them, and imagined that maybe one day we’d be as good as them.</p> <p>Over the years I’ve arranged and taught songs by Alice Cooper, The Postal Service, Imogen Heap, Bruce Springsteen, Chaka Khan, Wheatus, Regina Spektor, Dolly Parton, and more. Ain’t Nobody is well-known enough to most, and my arrangement simple enough, that for a while it became the first song anyone taught when a new branch of the choir popped up. It’s quite a feeling to stand on a stage in front of a hundred or so people from all over, most of whom you’ve never met, and conduct them all singing a song that you arranged.</p> <p>Conducting isn’t just waving your hands about and being a human metronome. It's conveying emotion, volume, shape of the sound. For me, the most special part of it is having your arrangement sung back at you with the full weight of the choir behind it – it often makes me emotional to hear those little crunchy chords I put in reflected so beautifully in the choir’s strong voices. <em>I did this.</em></p> <p>As time goes by the thing I enjoy the most is watching new conductors get that same joy from standing at the front and hearing their arrangement sung back at them. It can be a very vulnerable thing to put yourself at the front, and I’m so glad we’ve given people the opportunity to try new things out, arrange these songs nobody else knows but that mean so much to them, and learn new skills along the way.</p> <figure><picture><source type="image/webp" srcset="https://localghost.dev/img/L2U8Zbfvku-280.webp 280w, https://localghost.dev/img/L2U8Zbfvku-640.webp 640w, https://localghost.dev/img/L2U8Zbfvku-960.webp 960w" sizes="auto"><img loading="lazy" decoding="async" src="https://localghost.dev/img/L2U8Zbfvku-280.jpeg" alt="The choir performing in the South Bank centre, wearing all black and floral. Behind then, through the windows, you can see the Thames and buildings along the north bank of the river" width="960" height="720" srcset="https://localghost.dev/img/L2U8Zbfvku-280.jpeg 280w, https://localghost.dev/img/L2U8Zbfvku-640.jpeg 640w, https://localghost.dev/img/L2U8Zbfvku-960.jpeg 960w" sizes="auto"></picture></figure> <p>Three years after we attended the other choir’s rehearsal, we were on the same billing as them at a festival in London. I watched their set and realised that we were not only that good, but also so different. Going a cappella had actually forced us to be more creative with our arrangements, being the bass and the instruments as well.</p> <p>The choir that was once just in Manchester and London is now all over England, Germany and New Zealand. People love it so much that when they move away, they start up a new one wherever they end up. For a while there was one in a remote skiing town somewhere in France. Our London group is changing our name now, to better reflect the diversity of our members: we've called ourselves Mixtape. The perfect name for a weird and wonderful collection of songs, and people.</p> <p>On Thursday night at our winter gig I stood in front of the choir and waved my arms around to the tune of Dancing In The Dark by Bruce Springsteen, one of the most ambitious arrangements I’ve done – and the choir nailed every note. We’re an amateur choir in every way: no auditions, collectively and chaotically self-organised, and we’re all just making it up as we go along. But we’re so <em>good</em> at it.</p> <iframe width="560" height="315" src="https://www.youtube-nocookie.com/embed/Uwudzh3UjvQ?si=hJSrS-Kp1bCDXzLS" title="YouTube video player" frameborder="0" allow="accelerometer; autoplay; clipboard-write; encrypted-media; gyroscope; picture-in-picture; web-share" allowfullscreen=""></iframe> <p>I’ve invested a third of my life in this choir and there have been moments when I’ve been overcome with frustration (nobody’s doing anything! I have to do everything!) or worry (why hasn’t anyone learned the song?). But reflecting on how far we’ve come and what we’ve brought to people’s lives makes it all worth it.</p> <p>Those are the moments I live for: the biggest smile on a new conductor’s face as they conduct their song in public for the first time; the “I’m not sure if this is going to sound good” but it always does; the “okay, sure, I’ll have a go” from a quiet person; the drunken renditions of every song we’ve ever done, fuelled by too much wine, at every single choir retreat (and yes, it really is exactly like Pitch Perfect); and the teary reflections the morning of the last day of retreat as everyone shares what this choir means to them. And ultimately, it’s been a lifeline for me during some of the hardest times of my life.</p> <p>Of course, I could never convey all of that in one sentence when asked “what kind of choir is it?”. So I just say “it’s a pop choir.” But it’s a lot more than that, really, isn’t it?</p> <figure> <picture><source type="image/webp" srcset="https://localghost.dev/img/TS6pvItdlD-280.webp 280w, https://localghost.dev/img/TS6pvItdlD-640.webp 640w, https://localghost.dev/img/TS6pvItdlD-960.webp 960w" sizes="auto"><img loading="lazy" decoding="async" src="https://localghost.dev/img/TS6pvItdlD-280.jpeg" alt="Sophie in a knee length white wedding dress, surrounded by women in smart dresses, peparing to sing. they stand together in a converted barn, the exposed rafters visible above them." width="960" height="640" srcset="https://localghost.dev/img/TS6pvItdlD-280.jpeg 280w, https://localghost.dev/img/TS6pvItdlD-640.jpeg 640w, https://localghost.dev/img/TS6pvItdlD-960.jpeg 960w" sizes="auto"></picture> <figcaption>Photo: <a href="https://jessicajillphoto.com">Jessica Jill Photo</a></figcaption></figure> A good recipe: The best chocolate brownies 2023-11-23T00:00:00Z https://localghost.dev/blog/a-good-recipe-the-best-chocolate-brownies/ <p>I've been making this &quot;Bonfire Brownies&quot; recipe since it was aired on UK children's TV institution <a href="https://en.wikipedia.org/wiki/Blue_Peter">Blue Peter</a> in the early 00s, and I maintain it's still the best brownie recipe. Chewy and delicious. I recommend freezing them and eating them straight out of the freezer (I discovered this when my mum used to put them in the freezer to keep them out of the way of temptation, and I'd nick them when she wasn't looking). There's nothing bonfire-specific about it, they're chocolate brownies and I suppose the episode aired around 5th November, but they're fucking great.</p> <p>Look, I don't have any photos of this, but you've just got to take my word for it.</p> <p><strong>Makes 12</strong></p> <h3 id="ingredients" tabindex="-1">Ingredients</h3> <p>100g unsalted butter<br> 225g golden caster sugar (or regular caster sugar)<br> 40g cocoa powder<br> 2 eggs<br> 50g self-raising flour<br> 50g chocolate chips<br> 1/2 tsp vanilla essence (optional)</p> <h3 id="method" tabindex="-1">Method</h3> <p>Preheat the oven to 180°C/350°F. Grease and line a 20cm square brownie tin.</p> <p>In a large bowl beat the eggs, add the sugar and mix together until smooth.</p> <p>Melt the butter in a small saucepan over a medium heat. Sift in the cocoa powder and mix. Add this mixture along with the vanilla essence (if using) to the egg and sugar mixture.</p> <p>Sift the flour into the wet ingredients and mix together until smooth. Fold in the chocolate chips.</p> <p>Pour into the brownie tin, and put in the oven. Cook for around 20-25 mins, until the top is a darker brown (but watch it doesn't burn!).</p> <p>Leave to cool in the tin for 5-10 mins, then carefully turn out onto a cooling rack. For best results, wait until they're completely cooled to slice.</p> How to schedule posts in Eleventy 2023-11-21T00:00:00Z https://localghost.dev/blog/how-to-schedule-posts-in-eleventy/ <p>I scheduled this post, isn't that cute?</p> <p>I find I have very little energy to write blog posts during the week, but occasionally I sit down on the weekend to write something and have a few ideas that turn into multiple posts. I'd like to be able to publish posts more often, but realistically I won't be writing things more than once every few weeks (which is okay!). I decided I needed to be able to schedule posts so I don't just publish a load at once.</p> <p>Eleventy's suggested <a href="https://www.11ty.dev/docs/quicktips/draft-posts/">draft post setup</a> has been working nicely for me, and posts with <code>draft: true</code> in the frontmatter don't appear in the production site. However, I miss Hugo's scheduled posts feature, where I'd be able to write a post with a date in the future and it'd only appear on the blog after that date. Thankfully it's pretty straightforward to set up in Eleventy, in a similar way to drafts.</p> <h2 id="scheduling-website-builds" tabindex="-1">Scheduling website builds</h2> <p>My website already builds twice a day to fetch the latest webmentions. I had a GitHub Actions job to build and deploy my site to Neocities, with a cron schedule that ran every 12 hours:</p> <pre class="language-yaml"><code class="language-yaml"><span class="token key atrule">name</span><span class="token punctuation">:</span> <span class="token string">"Deploy to Neocities"</span> <span class="token key atrule">on</span><span class="token punctuation">:</span> <span class="token key atrule">schedule</span><span class="token punctuation">:</span> <span class="token punctuation">-</span> <span class="token key atrule">cron</span><span class="token punctuation">:</span> 0 <span class="token important">*/12</span> * * * <span class="token key atrule">push</span><span class="token punctuation">:</span> <span class="token key atrule">branches</span><span class="token punctuation">:</span> <span class="token punctuation">-</span> main <span class="token key atrule">jobs</span><span class="token punctuation">:</span> <span class="token key atrule">deploy</span><span class="token punctuation">:</span> <span class="token key atrule">name</span><span class="token punctuation">:</span> <span class="token string">"Deploy to Neocities"</span> <span class="token key atrule">runs-on</span><span class="token punctuation">:</span> ubuntu<span class="token punctuation">-</span>latest <span class="token key atrule">steps</span><span class="token punctuation">:</span> <span class="token punctuation">-</span> <span class="token key atrule">uses</span><span class="token punctuation">:</span> actions/checkout@v4 <span class="token punctuation">-</span> <span class="token key atrule">uses</span><span class="token punctuation">:</span> actions/setup<span class="token punctuation">-</span>node@v3 <span class="token key atrule">with</span><span class="token punctuation">:</span> <span class="token key atrule">node-version</span><span class="token punctuation">:</span> <span class="token string">"18"</span> <span class="token key atrule">cache</span><span class="token punctuation">:</span> <span class="token string">"yarn"</span> <span class="token punctuation">-</span> <span class="token key atrule">run</span><span class="token punctuation">:</span> yarn install <span class="token punctuation">-</span> <span class="token key atrule">run</span><span class="token punctuation">:</span> yarn build <span class="token punctuation">-</span> <span class="token key atrule">uses</span><span class="token punctuation">:</span> bcomnes/deploy<span class="token punctuation">-</span>to<span class="token punctuation">-</span>neocities@v1 <span class="token key atrule">with</span><span class="token punctuation">:</span> <span class="token key atrule">api_token</span><span class="token punctuation">:</span> $<span class="token punctuation">{</span><span class="token punctuation">{</span> secrets.SUPER_SECRET_TOKEN <span class="token punctuation">}</span><span class="token punctuation">}</span> <span class="token key atrule">dist_dir</span><span class="token punctuation">:</span> <span class="token string">"_site/"</span> <span class="token key atrule">cleanup</span><span class="token punctuation">:</span> <span class="token boolean important">false</span> </code></pre> <p>I tweaked the cron schedule so that it runs daily at 8am and 5pm, so I wouldn't publish a post at midnight.</p> <pre class="language-yaml"><code class="language-yaml"><span class="token key atrule">name</span><span class="token punctuation">:</span> <span class="token string">"Deploy to Neocities"</span> <span class="token key atrule">on</span><span class="token punctuation">:</span> <span class="token key atrule">schedule</span><span class="token punctuation">:</span> <span class="token punctuation">-</span> <span class="token key atrule">cron</span><span class="token punctuation">:</span> 0 8/9 * * *</code></pre> <h2 id="exclude-scheduled-posts-from-collections" tabindex="-1">Exclude scheduled posts from collections</h2> <p>In my <code>drafts.js</code> plugin, I added a new function to check whether a post's timestamp is in the future:</p> <pre class="language-js"><code class="language-js"><span class="token keyword">const</span> now <span class="token operator">=</span> <span class="token keyword">new</span> <span class="token class-name">Date</span><span class="token punctuation">(</span><span class="token punctuation">)</span><span class="token punctuation">;</span> <span class="token keyword">function</span> <span class="token function">isScheduledPost</span><span class="token punctuation">(</span><span class="token parameter">data</span><span class="token punctuation">)</span> <span class="token punctuation">{</span> <span class="token keyword">return</span> data<span class="token punctuation">.</span>date <span class="token operator">!=</span> <span class="token keyword">null</span> <span class="token operator">&amp;&amp;</span> data<span class="token punctuation">.</span>date <span class="token operator">></span> now<span class="token punctuation">;</span> <span class="token punctuation">}</span></code></pre> <p>Finally I added this function to the conditions in the <code>eleventyComputedPermalink</code> and <code>eleventyComputedExcludeFromCollections</code> functions which check for draft posts:</p> <pre class="language-js"><code class="language-js"><span class="token keyword">function</span> <span class="token function">eleventyComputedPermalink</span><span class="token punctuation">(</span><span class="token punctuation">)</span> <span class="token punctuation">{</span> <span class="token comment">// When using `addGlobalData` and you *want* to return a function, you must nest functions like this.</span> <span class="token comment">// `addGlobalData` acts like a global data file and runs the top level function it receives.</span> <span class="token keyword">return</span> <span class="token punctuation">(</span><span class="token parameter">data</span><span class="token punctuation">)</span> <span class="token operator">=></span> <span class="token punctuation">{</span> <span class="token comment">// Always skip during non-watch/serve builds</span> <span class="token keyword">if</span> <span class="token punctuation">(</span><span class="token operator">!</span>process<span class="token punctuation">.</span>env<span class="token punctuation">.</span><span class="token constant">BUILD_DRAFTS</span> <span class="token operator">&amp;&amp;</span> <span class="token punctuation">(</span>data<span class="token punctuation">.</span>draft <span class="token operator">||</span> <span class="token function">isScheduledPost</span><span class="token punctuation">(</span>data<span class="token punctuation">)</span><span class="token punctuation">)</span><span class="token punctuation">)</span> <span class="token punctuation">{</span> <span class="token keyword">return</span> <span class="token boolean">false</span><span class="token punctuation">;</span> <span class="token punctuation">}</span> <span class="token keyword">return</span> data<span class="token punctuation">.</span>permalink<span class="token punctuation">;</span> <span class="token punctuation">}</span><span class="token punctuation">;</span> <span class="token punctuation">}</span></code></pre> <p>That's it! The scheduled posts will always show up when running the site locally, as <code>process.env.BUILD_DRAFTS</code> is always true in the dev server. But they won't show up in the production site until the day comes and the site gets rebuilt. To be extra sure, you can run your site's prod build command and check the output – your scheduled page shouldn't be listed.</p> Building post types and category RSS feeds in Eleventy 2023-11-19T00:00:00Z https://localghost.dev/blog/building-post-types-and-category-rss-feeds-in-eleventy/ <p>I <a href="https://localghost.dev/blog/introducing-separate-category-rss-feeds/">mentioned recently</a> that I'd built separate RSS feeds for different kinds of posts. Here's how I did it!</p> <p>I had to do a bit of fiddling to get this working in Eleventy – this is one of the few things I think Hugo actually does in a simpler way, as you can create different <code>rss.xml</code> templates inside specific layout directories. My solution is not especially sophisticated, but it works! (If you know a less complicated way... I'd love to hear it.)</p> <h2 id="building-post-types" tabindex="-1">Building post types</h2> <p>Alongside the traditional long-form blog posts, I wanted to be able to publish little &quot;micro&quot; posts of recommendations and things I liked. In order to distinguish them, I added a new frontmatter field called <code>type</code> to all my posts. Right now, there are a few different types: article, podcast, game, book, and recipe.</p> <p>In my blog post list page, I add the <code>type</code> as a data attribute on the <code>&lt;li&gt;</code> for each &quot;micro&quot; post, so that I can display an appropriate emoji on the list item. (I haven't quite figured out how I want to show filters for post types yet.)</p> <p>Each post type has a json file which specifies the default frontmatter for all files in that directory. It automatically formats the post title into a consistent format, and tags the post accordingly. Here's <code>podcasts.json</code>:</p> <pre class="language-json"><code class="language-json"><span class="token punctuation">{</span> <span class="token property">"layout"</span><span class="token operator">:</span> <span class="token string">"good-podcast.njk"</span><span class="token punctuation">,</span> <span class="token property">"hasCustomOGImage"</span><span class="token operator">:</span> <span class="token boolean">true</span><span class="token punctuation">,</span> <span class="token property">"eleventyComputed"</span><span class="token operator">:</span> <span class="token punctuation">{</span> <span class="token property">"title"</span><span class="token operator">:</span> <span class="token string">"A good podcast: {{ podcast }}"</span> <span class="token punctuation">}</span><span class="token punctuation">,</span> <span class="token property">"type"</span><span class="token operator">:</span> <span class="token string">"podcast"</span><span class="token punctuation">,</span> <span class="token property">"tags"</span><span class="token operator">:</span> <span class="token punctuation">[</span> <span class="token string">"podcast"</span> <span class="token punctuation">]</span><span class="token punctuation">,</span> <span class="token property">"permalink"</span><span class="token operator">:</span> <span class="token string">"/blog/a-good-podcast-{{ podcast | slugify }}/index.html"</span> <span class="token punctuation">}</span></code></pre> <p>The tags and type are identical because I wanted a quick way of accessing post type in things like the layout, but I wanted readers to be able to list all podcasts just like they can any other tag – and post tags might contain more than just &quot;podcast&quot;, say. Any tags I set on the post itself will be added to that array, not replace it.</p> <p>I use the <code>hasCustomOGImage</code> variable to tell a layout whether to find a specific OG image based on the title of the post, or just use the default one.</p> <p>The post itself has its own frontmatter, with the name of the podcast (or book, or game, etc), the date, an image with alt text, and the URL of the podcast.</p> <pre class="language-md"><code class="language-md"><span class="token front-matter-block"><span class="token punctuation">---</span> <span class="token front-matter yaml language-yaml"><span class="token key atrule">podcast</span><span class="token punctuation">:</span> If Books Could Kill <span class="token key atrule">url</span><span class="token punctuation">:</span> https<span class="token punctuation">:</span>//pod.link/1651876897 <span class="token key atrule">date</span><span class="token punctuation">:</span> <span class="token datetime number">2023-07-16</span> <span class="token key atrule">image</span><span class="token punctuation">:</span> if<span class="token punctuation">-</span>books<span class="token punctuation">-</span>could<span class="token punctuation">-</span>kill.jpeg <span class="token key atrule">alt</span><span class="token punctuation">:</span> <span class="token string">"The cover image for If Books Could Kill, with an illustration of a bleeding book"</span></span> <span class="token punctuation">---</span></span> Michael Hobbes and Peter Shamshiri take us through some of the biggest "self-help"/pseudoscience books from the last few decades and deservedly tear them apart. [...]</code></pre> <h2 id="separating-rss-feeds" tabindex="-1">Separating RSS feeds</h2> <p>If you don't already, you'll need the Eleventy RSS plugin set up. See the <a href="https://www.11ty.dev/docs/plugins/rss/">plugin docs</a> for instructions.</p> <p>My RSS feed was previously one single feed, set up with some basic settings – it was pretty much identical to the <a href="https://www.11ty.dev/docs/plugins/rss/#sample-feed-templates">example in the RSS plugin docs</a>.</p> <p>I wanted to split up my feeds into the various different types of &quot;micro&quot; posts (the recommendations), and then everything else as one &quot;articles&quot; feed; however, I also wanted to keep the &quot;everything&quot; feed for people who like their posts about Content-Security-Policy headers interspersed with brownie recipes from 2001.</p> <h3 id="collections-collections-collections" tabindex="-1">Collections, collections, collections</h3> <p>I've got a few custom collections for my post types. The first one pulls <em>all</em> of the posts in the <code>blog</code> directory, which encompasses all articles and post types:</p> <pre class="language-js"><code class="language-js"><span class="token comment">// eleventy.config.js</span> eleventyConfig<span class="token punctuation">.</span><span class="token function">addCollection</span><span class="token punctuation">(</span><span class="token string">"blog"</span><span class="token punctuation">,</span> <span class="token punctuation">(</span><span class="token parameter">collection</span><span class="token punctuation">)</span> <span class="token operator">=></span> <span class="token punctuation">{</span> <span class="token keyword">return</span> collection<span class="token punctuation">.</span><span class="token function">getFilteredByGlob</span><span class="token punctuation">(</span><span class="token string">"./src/blog/**/*.md"</span><span class="token punctuation">)</span><span class="token punctuation">;</span> <span class="token punctuation">}</span><span class="token punctuation">)</span><span class="token punctuation">;</span></code></pre> <p>Then I've got the <code>categoryFeeds</code> collection which contains all of the specific feeds I want for each &quot;micro&quot; post type.</p> <pre class="language-js"><code class="language-js">eleventyConfig<span class="token punctuation">.</span><span class="token function">addCollection</span><span class="token punctuation">(</span><span class="token string">"categoryFeeds"</span><span class="token punctuation">,</span> <span class="token punctuation">(</span><span class="token punctuation">)</span> <span class="token operator">=></span> <span class="token punctuation">[</span><span class="token string">"recipe"</span><span class="token punctuation">,</span> <span class="token string">"book"</span><span class="token punctuation">,</span> <span class="token string">"game"</span><span class="token punctuation">,</span> <span class="token string">"podcast"</span><span class="token punctuation">]</span><span class="token punctuation">)</span><span class="token punctuation">;</span></code></pre> <p>Finally, I've got one for &quot;articles&quot; - all the posts that <em>aren't</em> micro-categories. These all live in the same directory, so I used <code>getFilteredByGlob</code> again. (I've actually called the directory &quot;posts&quot; but I couldn't be bothered to change it.)</p> <pre class="language-js"><code class="language-js"> eleventyConfig<span class="token punctuation">.</span><span class="token function">addCollection</span><span class="token punctuation">(</span><span class="token string">"articles"</span><span class="token punctuation">,</span> <span class="token punctuation">(</span><span class="token parameter">collection</span><span class="token punctuation">)</span> <span class="token operator">=></span> <span class="token punctuation">{</span> <span class="token keyword">return</span> collection<span class="token punctuation">.</span><span class="token function">getFilteredByGlob</span><span class="token punctuation">(</span><span class="token string">"./src/blog/posts/*.md"</span><span class="token punctuation">)</span><span class="token punctuation">;</span> <span class="token punctuation">}</span><span class="token punctuation">)</span><span class="token punctuation">;</span></code></pre> <h3 id="paginating-the-category-feeds" tabindex="-1">Paginating the category feeds</h3> <p>I used Eleventy's excellent pagination feature to generate the RSS feeds themselves. I've got a file called <code>rss-categories.11ty.js</code> which contains info about the data I want to paginate, and that will produce a feed for each of the feed categories in my <code>categoryFeeds</code> collection. Each generated file will have the same template (<code>rss.njk</code>) but a different title, subtitle and feed URL; it'll also have a different value of <code>feed</code> (set to whichever category the feed is for, based on the collection item).</p> <pre class="language-js"><code class="language-js"><span class="token keyword">class</span> <span class="token class-name">RSSCategories</span> <span class="token punctuation">{</span> <span class="token function">data</span><span class="token punctuation">(</span><span class="token punctuation">)</span> <span class="token punctuation">{</span> <span class="token keyword">return</span> <span class="token punctuation">{</span> <span class="token literal-property property">pagination</span><span class="token operator">:</span> <span class="token punctuation">{</span> <span class="token literal-property property">data</span><span class="token operator">:</span> <span class="token string">"collections.categoryFeeds"</span><span class="token punctuation">,</span> <span class="token literal-property property">size</span><span class="token operator">:</span> <span class="token number">1</span><span class="token punctuation">,</span> <span class="token literal-property property">alias</span><span class="token operator">:</span> <span class="token string">"feed"</span><span class="token punctuation">,</span> <span class="token punctuation">}</span><span class="token punctuation">,</span> <span class="token function-variable function">permalink</span><span class="token operator">:</span> <span class="token punctuation">(</span><span class="token parameter"><span class="token punctuation">{</span> feed <span class="token punctuation">}</span></span><span class="token punctuation">)</span> <span class="token operator">=></span> <span class="token template-string"><span class="token template-punctuation string">`</span><span class="token string">/</span><span class="token interpolation"><span class="token interpolation-punctuation punctuation">${</span>feed<span class="token interpolation-punctuation punctuation">}</span></span><span class="token string">s.xml</span><span class="token template-punctuation string">`</span></span><span class="token punctuation">,</span> <span class="token literal-property property">eleventyExcludeFromCollections</span><span class="token operator">:</span> <span class="token boolean">true</span><span class="token punctuation">,</span> <span class="token literal-property property">layout</span><span class="token operator">:</span> <span class="token string">"rss.njk"</span><span class="token punctuation">,</span> <span class="token literal-property property">eleventyComputed</span><span class="token operator">:</span> <span class="token punctuation">{</span> <span class="token function-variable function">title</span><span class="token operator">:</span> <span class="token punctuation">(</span><span class="token parameter"><span class="token punctuation">{</span> feed <span class="token punctuation">}</span></span><span class="token punctuation">)</span> <span class="token operator">=></span> <span class="token template-string"><span class="token template-punctuation string">`</span><span class="token string">localghost.dev - posts about </span><span class="token interpolation"><span class="token interpolation-punctuation punctuation">${</span>feed<span class="token interpolation-punctuation punctuation">}</span></span><span class="token string">s</span><span class="token template-punctuation string">`</span></span><span class="token punctuation">,</span> <span class="token function-variable function">subtitle</span><span class="token operator">:</span> <span class="token punctuation">(</span><span class="token parameter"><span class="token punctuation">{</span> feed <span class="token punctuation">}</span></span><span class="token punctuation">)</span> <span class="token operator">=></span> <span class="token template-string"><span class="token template-punctuation string">`</span><span class="token string">A feed of the latest </span><span class="token interpolation"><span class="token interpolation-punctuation punctuation">${</span>feed<span class="token interpolation-punctuation punctuation">}</span></span><span class="token string"> recommendations from localghost.dev</span><span class="token template-punctuation string">`</span></span><span class="token punctuation">,</span> <span class="token function-variable function">feedUrl</span><span class="token operator">:</span> <span class="token punctuation">(</span><span class="token parameter"><span class="token punctuation">{</span> feed <span class="token punctuation">}</span></span><span class="token punctuation">)</span> <span class="token operator">=></span> <span class="token template-string"><span class="token template-punctuation string">`</span><span class="token string">https://localghost.dev/</span><span class="token interpolation"><span class="token interpolation-punctuation punctuation">${</span>feed<span class="token interpolation-punctuation punctuation">}</span></span><span class="token string">s.xml</span><span class="token template-punctuation string">`</span></span><span class="token punctuation">,</span> <span class="token punctuation">}</span><span class="token punctuation">,</span> <span class="token punctuation">}</span><span class="token punctuation">;</span> <span class="token punctuation">}</span> <span class="token function">render</span><span class="token punctuation">(</span><span class="token punctuation">)</span> <span class="token punctuation">{</span> <span class="token keyword">return</span> <span class="token keyword">null</span><span class="token punctuation">;</span> <span class="token punctuation">}</span> <span class="token punctuation">}</span> module<span class="token punctuation">.</span>exports <span class="token operator">=</span> RSSCategories<span class="token punctuation">;</span></code></pre> <h3 id="one-rss-template-for-all" tabindex="-1">One RSS template for all</h3> <p>My RSS feed template lives in <code>_includes/rss.njk</code> alongside all my other layouts. It's pretty much the standard RSS template they suggest in the docs, but with a couple of tweaks to make it a bit more generic, allowing <code>title</code>, <code>subtitle</code> and <code>feedUrl</code> to be optionally passed in as variables.</p> <pre class="language-njk"><code class="language-njk"><span class="token operator">&lt;</span>?<span class="token variable">xml</span> <span class="token variable">version</span><span class="token operator">=</span><span class="token string">"1.0"</span> <span class="token variable">encoding</span><span class="token operator">=</span><span class="token string">"utf-8"</span>?<span class="token operator">></span> <span class="token punctuation">{</span><span class="token operator">%</span> <span class="token keyword">if</span> <span class="token keyword">not</span> <span class="token variable">title</span> <span class="token operator">%</span><span class="token punctuation">}</span> <span class="token punctuation">{</span><span class="token operator">%</span> <span class="token variable">set</span> <span class="token variable">title</span> <span class="token operator">=</span> <span class="token variable">metadata</span><span class="token punctuation">.</span><span class="token variable">title</span> <span class="token operator">%</span><span class="token punctuation">}</span> <span class="token punctuation">{</span><span class="token operator">%</span> <span class="token variable">endif</span> <span class="token operator">%</span><span class="token punctuation">}</span> <span class="token punctuation">{</span><span class="token operator">%</span> <span class="token keyword">if</span> <span class="token keyword">not</span> <span class="token variable">subtitle</span> <span class="token operator">%</span><span class="token punctuation">}</span> <span class="token punctuation">{</span><span class="token operator">%</span> <span class="token variable">set</span> <span class="token variable">subtitle</span> <span class="token operator">=</span> <span class="token variable">metadata</span><span class="token punctuation">.</span><span class="token variable">description</span> <span class="token operator">%</span><span class="token punctuation">}</span> <span class="token punctuation">{</span><span class="token operator">%</span> <span class="token variable">endif</span> <span class="token operator">%</span><span class="token punctuation">}</span> <span class="token operator">&lt;</span><span class="token variable">feed</span> <span class="token variable">xmlns</span><span class="token operator">=</span><span class="token string">"http://www.w3.org/2005/Atom"</span><span class="token operator">></span> <span class="token operator">&lt;</span><span class="token variable">title</span><span class="token operator">></span><span class="token punctuation">{</span><span class="token punctuation">{</span> <span class="token variable">title</span> <span class="token punctuation">}</span><span class="token punctuation">}</span><span class="token operator">&lt;</span><span class="token operator">/</span><span class="token variable">title</span><span class="token operator">></span> <span class="token operator">&lt;</span><span class="token variable">subtitle</span><span class="token operator">></span><span class="token punctuation">{</span><span class="token punctuation">{</span> <span class="token variable">subtitle</span> <span class="token punctuation">}</span><span class="token punctuation">}</span><span class="token operator">&lt;</span><span class="token operator">/</span><span class="token variable">subtitle</span><span class="token operator">></span> <span class="token operator">&lt;</span><span class="token variable">link</span> <span class="token variable">href</span><span class="token operator">=</span><span class="token string">"{{ feedUrl }}"</span> <span class="token variable">rel</span><span class="token operator">=</span><span class="token string">"self"</span><span class="token operator">/</span><span class="token operator">></span> <span class="token operator">&lt;</span><span class="token variable">link</span> <span class="token variable">href</span><span class="token operator">=</span><span class="token string">"{{ metadata.url }}"</span><span class="token operator">/</span><span class="token operator">></span> <span class="token operator">&lt;</span><span class="token variable">updated</span><span class="token operator">></span><span class="token punctuation">{</span><span class="token punctuation">{</span> <span class="token variable">collections</span><span class="token punctuation">[</span><span class="token variable">feed</span><span class="token punctuation">]</span> <span class="token operator">|</span> <span class="token variable">getNewestCollectionItemDate</span> <span class="token operator">|</span> <span class="token variable">dateToRfc3339</span> <span class="token punctuation">}</span><span class="token punctuation">}</span><span class="token operator">&lt;</span><span class="token operator">/</span><span class="token variable">updated</span><span class="token operator">></span> <span class="token operator">&lt;</span><span class="token variable">id</span><span class="token operator">></span><span class="token punctuation">{</span><span class="token punctuation">{</span> <span class="token variable">metadata</span><span class="token punctuation">.</span><span class="token variable">url</span> <span class="token punctuation">}</span><span class="token punctuation">}</span><span class="token operator">&lt;</span><span class="token operator">/</span><span class="token variable">id</span><span class="token operator">></span> <span class="token operator">&lt;</span><span class="token variable">author</span><span class="token operator">></span> <span class="token operator">&lt;</span><span class="token variable">name</span><span class="token operator">></span><span class="token punctuation">{</span><span class="token punctuation">{</span><span class="token variable">metadata</span><span class="token punctuation">.</span><span class="token variable">author</span><span class="token punctuation">.</span><span class="token variable">name</span><span class="token punctuation">}</span><span class="token punctuation">}</span><span class="token operator">&lt;</span><span class="token operator">/</span><span class="token variable">name</span><span class="token operator">></span> <span class="token operator">&lt;</span><span class="token variable">email</span><span class="token operator">></span><span class="token punctuation">{</span><span class="token punctuation">{</span><span class="token variable">metadata</span><span class="token punctuation">.</span><span class="token variable">author</span><span class="token punctuation">.</span><span class="token variable">email</span><span class="token punctuation">}</span><span class="token punctuation">}</span><span class="token operator">&lt;</span><span class="token operator">/</span><span class="token variable">email</span><span class="token operator">></span> <span class="token operator">&lt;</span><span class="token operator">/</span><span class="token variable">author</span><span class="token operator">></span> <span class="token punctuation">{</span><span class="token operator">%</span><span class="token operator">-</span> <span class="token keyword">for</span> <span class="token variable">post</span> <span class="token keyword">in</span> <span class="token variable">collections</span><span class="token punctuation">[</span><span class="token variable">feed</span><span class="token punctuation">]</span> <span class="token operator">%</span><span class="token punctuation">}</span> <span class="token punctuation">{</span><span class="token operator">%</span> <span class="token variable">set</span> <span class="token variable">absolutePostUrl</span> <span class="token operator">%</span><span class="token punctuation">}</span><span class="token punctuation">{</span><span class="token punctuation">{</span> <span class="token variable">post</span><span class="token punctuation">.</span><span class="token variable">url</span> <span class="token operator">|</span> <span class="token variable">url</span> <span class="token operator">|</span> <span class="token function">absoluteUrl</span><span class="token punctuation">(</span><span class="token variable">metadata</span><span class="token punctuation">.</span><span class="token variable">url</span><span class="token punctuation">)</span> <span class="token punctuation">}</span><span class="token punctuation">}</span><span class="token punctuation">{</span><span class="token operator">%</span> <span class="token variable">endset</span> <span class="token operator">%</span><span class="token punctuation">}</span> <span class="token operator">&lt;</span><span class="token variable">entry</span><span class="token operator">></span> <span class="token operator">&lt;</span><span class="token variable">title</span><span class="token operator">></span><span class="token punctuation">{</span><span class="token punctuation">{</span> <span class="token variable">post</span><span class="token punctuation">.</span><span class="token variable">data</span><span class="token punctuation">.</span><span class="token variable">title</span> <span class="token punctuation">}</span><span class="token punctuation">}</span><span class="token operator">&lt;</span><span class="token operator">/</span><span class="token variable">title</span><span class="token operator">></span> <span class="token operator">&lt;</span><span class="token variable">link</span> <span class="token variable">href</span><span class="token operator">=</span><span class="token string">"{{ absolutePostUrl }}"</span><span class="token operator">/</span><span class="token operator">></span> <span class="token operator">&lt;</span><span class="token variable">updated</span><span class="token operator">></span><span class="token punctuation">{</span><span class="token punctuation">{</span> <span class="token variable">post</span><span class="token punctuation">.</span><span class="token variable">date</span> <span class="token operator">|</span> <span class="token variable">dateToRfc3339</span> <span class="token punctuation">}</span><span class="token punctuation">}</span><span class="token operator">&lt;</span><span class="token operator">/</span><span class="token variable">updated</span><span class="token operator">></span> <span class="token operator">&lt;</span><span class="token variable">id</span><span class="token operator">></span><span class="token punctuation">{</span><span class="token punctuation">{</span> <span class="token variable">absolutePostUrl</span> <span class="token punctuation">}</span><span class="token punctuation">}</span><span class="token operator">&lt;</span><span class="token operator">/</span><span class="token variable">id</span><span class="token operator">></span> <span class="token operator">&lt;</span><span class="token variable">content</span> <span class="token variable">type</span><span class="token operator">=</span><span class="token string">"html"</span><span class="token operator">></span><span class="token punctuation">{</span><span class="token punctuation">{</span> <span class="token variable">post</span><span class="token punctuation">.</span><span class="token variable">templateContent</span> <span class="token operator">|</span> <span class="token function">htmlToAbsoluteUrls</span><span class="token punctuation">(</span><span class="token variable">absolutePostUrl</span><span class="token punctuation">)</span> <span class="token punctuation">}</span><span class="token punctuation">}</span><span class="token operator">&lt;</span><span class="token operator">/</span><span class="token variable">content</span><span class="token operator">></span> <span class="token operator">&lt;</span><span class="token operator">/</span><span class="token variable">entry</span><span class="token operator">></span> <span class="token punctuation">{</span><span class="token operator">%</span><span class="token operator">-</span> <span class="token variable">endfor</span> <span class="token operator">%</span><span class="token punctuation">}</span> <span class="token operator">&lt;</span><span class="token operator">/</span><span class="token variable">feed</span><span class="token operator">></span></code></pre> <p>Oddly, the variables seem to leave a blank space in the outputted XML, but as long as the <code>&lt;?xml&gt;</code> declaration is at the top it doesn't seem to matter.</p> <p>The RSS template iterates over a given collection and includes all the posts it finds, so I have to tell it which collection to pull from. This is where the <code>feed</code> variable comes in. It tells the template to iterate over posts tagged with e.g. &quot;recipe&quot; or &quot;game&quot;.</p> <h3 id="a-separate-articles-feed" tabindex="-1">A separate articles feed</h3> <p>Unlike my specific category fields, &quot;everything else&quot; isn't a tag, so I can't use the same pagination to generate the &quot;articles&quot; feed. That has to have its own file. Since it hasn't got any fancy variable templating, I can just create a nunjucks file and fill out the frontmatter:</p> <pre class="language-md"><code class="language-md"><span class="token front-matter-block"><span class="token punctuation">---</span> <span class="token front-matter yaml language-yaml"><span class="token key atrule">permalink</span><span class="token punctuation">:</span> <span class="token string">'articles.xml'</span> <span class="token key atrule">eleventyExcludeFromCollections</span><span class="token punctuation">:</span> <span class="token boolean important">true</span> <span class="token key atrule">layout</span><span class="token punctuation">:</span> <span class="token string">"rss.njk"</span> <span class="token key atrule">feed</span><span class="token punctuation">:</span> articles <span class="token key atrule">title</span><span class="token punctuation">:</span> <span class="token string">"localghost.dev - all articles"</span> <span class="token key atrule">subtitle</span><span class="token punctuation">:</span> <span class="token string">"Sophie builds fun things out of HTML, CSS &amp; JavaScript, and writes blog posts about tech and mental health. This feed excludes recommendations about books, games, podcasts, recipes, etc."</span> <span class="token key atrule">feedUrl</span><span class="token punctuation">:</span> <span class="token string">"https://localghost.dev/articles.xml"</span></span> <span class="token punctuation">---</span></span> <span class="token comment">&lt;!-- rss-articles.njk --></span></code></pre> <p>Because I created the <code>articles</code> category in my Eleventy config, I can pass that in as the <code>feed</code> variable, and then <code>collections[feed]</code> in the RSS layout will translate to <code>collections.articles</code>. The only posts in this feed will be anything that lives in my <code>/blog/posts/</code> folder.</p> <h2 id="the-primary-feed" tabindex="-1">The primary feed</h2> <p>And finally, the tweak to my &quot;everything&quot; feed to get it to work with this new layout: make sure it also has a <code>feed</code> variable. The <code>blog</code> collection already exists, and contains everything in the <code>blog</code> directory, so we can pass that in and it'll add everything in there to the RSS feed (which is actually what it was doing before, anyway).<br> The metadata for my primary RSS feed is shared with the meta tags for the <code>&lt;head&gt;</code> of my site, so it lives elsewhere in <code>_data</code>.</p> <pre class="language-md"><code class="language-md"><span class="token front-matter-block"><span class="token punctuation">---</span> <span class="token front-matter yaml language-yaml"><span class="token key atrule">permalink</span> <span class="token punctuation">:</span> <span class="token string">"feed.xml"</span> <span class="token key atrule">eleventyExcludeFromCollections</span> <span class="token punctuation">:</span> <span class="token boolean important">true</span> <span class="token key atrule">feed</span><span class="token punctuation">:</span> <span class="token string">"blog"</span> <span class="token key atrule">layout</span><span class="token punctuation">:</span> <span class="token string">"rss.njk"</span> <span class="token key atrule">feedUrl</span><span class="token punctuation">:</span> <span class="token string">"https://localghost.dev/feed.xml"</span></span> <span class="token punctuation">---</span></span> <span class="token comment">&lt;!-- rss-all.njk --></span></code></pre> <p>And there you go! Multiple RSS feeds. I changed the <a href="https://localghost.dev/rss">RSS link</a> in my footer to point towards a page where I added links to all the feeds.</p> A good recipe: Smitten Kitchen's apple pie cookies 2023-11-12T00:00:00Z https://localghost.dev/blog/a-good-recipe-smitten-kitchens-apple-pie-cookies/ <p>If I need to impress, or bribe people to like me, I make these cookies. A shortcrust pastry/biscuit dough hybrid case with appley-cinnamony goodness inside, packaged in an adorable pie shape.</p> <p>The process is a little involved, but they're flaky and delicious (and often gone in an instant). I first made them over a decade ago and it's still one of my go-to recipes.</p> Introducing separate category RSS feeds 2023-11-12T00:00:00Z https://localghost.dev/blog/introducing-separate-category-rss-feeds/ <p>Recently I've been inspired by folks like <a href="https://css-irl.info">Michelle Barker</a> and <a href="https://amyhupe.co.uk/">Amy Hupe</a> doing National Blog Posting Month (NaBloPoMo!), and seeing folks writing down little thoughts and things they've encountered. I'm absolutely not doing NaBloPoMo, I am far too tired, but I thought I'd make it easier for myself to write things down a bit more often than once every three to six months.</p> <p>I've introduced some new categories to my blog, where I can share things like books, games and podcasts I've enjoyed recently, and recipes I love. I figured that having these little bites to post occasionally might spur me to write a bit more often. However, I know people won't necessarily want to <em>read</em> all of those things if they subscribe to my RSS feed. I also have very specific categories in my own RSS reader, so I wanted to make sure people can only subscribe to the things that interest them. So I built separate RSS feeds!</p> <p>My site's main feed still contains <em>everything</em>, but if you don't want the occasional brownie recipe with your accessibility rants, you can subscribe to my &quot;articles&quot; feed which only has my writings and rantings. If you want to get a podcast recommendation now and then, the &quot;podcasts&quot; feed is for you. The new feeds are a bit quiet at the moment, but will pick up as I post more!</p> <p>I'm writing up how I built it, but for now you can pick your favourite feeds on the <a href="https://localghost.dev/rss">RSS page</a>.</p> "AI", and the trouble with inaccessible SaaS 2023-09-17T00:00:00Z https://localghost.dev/blog/ai-and-the-trouble-with-inaccessible-saas/ <p>I wasn't particularly enthused by the alpha release of <a href="https://v0.dev">Vercel's v0 UI generation tool</a> this week. Provided with a prompt, it produces React code with Tailwind to style it. It's being marketed as a prototyping tool, but was described by Vercel's CEO on Twitter as <a href="https://twitter.com/rauchg/status/1702355455362912595">&quot;production-grade&quot;</a>. Well, which one is it?</p> <p>To be clear, my issue isn't with the concept of large language models (LLMs) in general: I use Copilot in my everyday work, and it's saved me a lot of time when writing tests! <strong>My concern is with the source material used to train it, and what people do with the output.</strong></p> <p>Hidde de Vries wrote a <a href="https://hidde.blog/interactions-about-accessibility/">great post on the incredibly problematic dev community response</a> after the accessibility of the UI was called out, so I won't go into that here.</p> <h2 id="stop-putting-inaccessible-prototypes-into-production" tabindex="-1">Stop putting inaccessible prototypes into production</h2> <p>Prototyping is one thing. Noodling around with UI ideas? Fantastic. Please don't put it into production.</p> <p>A pattern I've noticed with a huge number of tech/SaaS startups is:</p> <ul> <li>have idea</li> <li>build very quick, inaccessible UI &quot;prototype&quot; to test idea</li> <li>ship prototype to production</li> <li>prototype becomes the actual product</li> </ul> <p>Inevitably, people will copy existing patterns in the codebase, adding more stuff on top, and time passes. At this point, you have a completely inaccessible SaaS product, and the amount of work needed to go back and fix all the problems is so much, and so costly, that it's never going to be a &quot;business priority&quot;. <em>So many</em> of the B2B SaaS tools I've used are terribly inaccessible; companies seem to forget that staff may have access needs, as well as customers, and that those access needs may extend beyond being blind or partially sighted.</p> <p>In fact, a 2020 study by RNIB (<a href="https://media.rnib.org.uk/documents/Employment_facts_and_stats_2020_-_External_version.docx#:~:text=There%20is%20a%20significant%20employment,and%20partially%20sighted%20%5B7%5D">docx</a>) found that &quot;50% of employers thought that there may be additional health and safety risks in the workplace [employing someone blind or partially sighted]; 33% of employers thought that they may not be able to operate a computer/laptop; 33% of employers thought that they may not be able to operate the necessary equipment, excluding computers/laptops&quot;. Isn't that just terribly fucking depressing?</p> <p>I truly wish the founders of SaaS companies worldwide would learn some proper semantic HTML before they build these UIs that are going to form the basis of their entire product for years to come. I'd love to see SaaS accessibility viewed as a competitive advantage rather than an expensive afterthought.</p> <p>As <a href="https://twitter.com/AshleeMBoyer/status/1702379264836882623">Ashlee Boyer</a> puts it:</p> <blockquote> <p>The right time to work on accessibility is before you launch a product into alpha.</p> </blockquote> <p>Before v0 was announced I was planning on writing a blog post about the inaccessibility of SaaS software, and how it starts at prototypes making it to production, so I guess this is that post. The problem has existed for years, long before generative LLMs came along. LLMs are just making it worse, and faster.</p> <h2 id="people-write-inaccessible-code-and-ll-ms-copy-people" tabindex="-1">People write inaccessible code, and LLMs copy people</h2> <p>Tools like v0 are destined to become the new &quot;copying and pasting from open-source UI libraries&quot;. In the wider community, ChatGPT has already overtaken StackOverflow as the first port of call for coding questions. Developers shouldn't unreservedly trust the output from code-generating LLMs, though. I double-triple-check everything that Copilot produces for me, and generally end up tweaking it about half of the time – and I rarely let it write blocks of code unless I'm writing loads of repetitive tests. It's great for autocompleting variables, repeating things I've already written and finishing lines, but I have found it has a tendency to make up functions that don't exist.</p> <p>Likewise, ChatGPT and v0 may produce decent enough code, or they might give you <a href="https://v0.dev/t/LnxRCcq"><code>&lt;div&gt;</code>s that should be links</a>. At least v0 seems to be adding labels to form inputs, which is more than I can say for a lot of developers on the web. But that's kind of the issue: people don't write accessible code, so why should we be trusting algorithms trained on other people's inaccessible code to write UI code for us? Vercel say it was trained on code written by their team, but considering the UI for v0 <a href="https://twitter.com/AshleeMBoyer/status/1702367107130720534">wasn't accessible itself</a>, I don't have a huge amount of confidence that the input source material for the LLM was.</p> <p>This isn't purely a Vercel problem, of course, it's an everyone problem. But we as developers can make it better by <em>learning how to write semantic HTML and proper CSS</em>, and then training the models on <em>that</em>.</p> <p>I do believe that LLM-backed tools can speed up the development process and make us more productive, but not through developers indiscriminately pasting whatever they generate into our codebases and trusting that it's all fine. Our users deserve a little more diligence than that.</p> A good podcast: If Books Could Kill 2023-07-16T00:00:00Z https://localghost.dev/blog/a-good-podcast-if-books-could-kill/ <p>Michael Hobbes and Peter Shamshiri take us through some of the biggest &quot;self-help&quot;/pseudoscience books from the last few decades and deservedly tear them apart.</p> <p>Some of the worst relationship books are here – <em>Men Are From Mars, Women Are From Venus</em>, <em>The 5 Love Languages</em>, <em>The Rules</em> and <em>The Game</em> (yes, that dreadful pickup artist one) – alongside pop-economics and pop-psychology rubbish such as <em>Freakonomics</em> and <em>Atomic Habits</em>. One of the funniest podcasts I've listened to in a while, and honestly gobsmacking that these books were so well-received in the first place. I feel like this should be called &quot;Citation Needed: The Podcast&quot;.</p> Beyond Tellerrand: beyond amazing 2023-05-01T00:00:00Z https://localghost.dev/blog/beyond-tellerrand-beyond-amazing/ <p>I had the absolute privilege of opening Beyond Tellerrand Düsseldorf recently. Truth be told, I had no idea what to expect, other than a great conference (from what everyone had told me), but it was incredible! (Apologies to anyone who tried to chat to me on the Monday after my talk – if I seemed a bit distant, it was because I was literally struggling to string words together because of adrenaline and tiredness.) It took me a week to recover from the travel, socialising and speaking, and I went straight back to work so I'm only getting around to writing this now!</p> <p>From the first conversation with <a href="http://www.marcthiele.com/">Marc</a> I knew this was something special: he and the Beyond Tellerrand team put so much love into this event, and it really shows. The venue is awesome, the production quality is amazing, and the speakers are treated really well. I arrived at the hotel exhausted after a day of travel to find a handwritten letter from Marc, a <a href="https://www.shauntan.net/arrival-book">beautiful book</a>, and a very useful backpack which will be accompanying me to all my future conferences! The team and the speakers had a wonderful meal the night before the conference, and the first talk wasn't until 11am which meant I didn't have to panic-inhale my breakfast and leg it to the venue for an early start. I never sleep well before giving talks, so the extra time was very welcome.</p> <figure> <a href="https://localghost.dev/img/blog/beyond-tellerrand-2023/btconf-me-stage.jpg" target="_blank"><picture><source type="image/webp" srcset="https://localghost.dev/img/j_QANK8sQw-280.webp 280w, https://localghost.dev/img/j_QANK8sQw-640.webp 640w, https://localghost.dev/img/j_QANK8sQw-960.webp 960w" sizes="auto"><img loading="lazy" decoding="async" src="https://localghost.dev/img/j_QANK8sQw-280.jpeg" alt="Me on the stage" width="960" height="639" srcset="https://localghost.dev/img/j_QANK8sQw-280.jpeg 280w, https://localghost.dev/img/j_QANK8sQw-640.jpeg 640w, https://localghost.dev/img/j_QANK8sQw-960.jpeg 960w" sizes="auto"></picture></a> <figcaption>Photo credit: <a href="https://florian.photo/" target="_blank" rel="noreferrer noopener">Florian Ziegler</a></figcaption> </figure> <p>Speaking of the incredible production quality: Marc got dina Amin to do these <a href="https://youtu.be/SfIzk_9fdYs">unbelievably cool stop-motion videos of our names</a> (by the way, she's the loveliest person and I'm so excited to see her speak at All Day Hey next week). Even more impressive: there were posters for each of our talks, and they were AR-enabled! I managed to get my poster home on the plane, and I've framed it by my desk.</p> <div class="content-grid"> <a href="https://localghost.dev/img/blog/beyond-tellerrand-2023/bt-poster.JPG" target="_blank"><picture><source type="image/webp" srcset="https://localghost.dev/img/AHZ1t2pERm-280.webp 280w, https://localghost.dev/img/AHZ1t2pERm-640.webp 640w, https://localghost.dev/img/AHZ1t2pERm-960.webp 960w" sizes="auto"><img loading="lazy" decoding="async" src="https://localghost.dev/img/AHZ1t2pERm-280.jpeg" alt="My Beyond Tellerrand poster hanging up in the venue. It says 'A Beyond Tellerrand Film' and underneath has the title of my talk - 'This website is under construction - a love letter to the personal website'. Then it has a series of squares with household objects in, that when you scan it with a special app move to spell my name. Underneath are credits for the conference." width="960" height="1280" srcset="https://localghost.dev/img/AHZ1t2pERm-280.jpeg 280w, https://localghost.dev/img/AHZ1t2pERm-640.jpeg 640w, https://localghost.dev/img/AHZ1t2pERm-960.jpeg 960w" sizes="auto"></picture></a> <video controls=""> <source src="https://localghost.dev/img/blog/beyond-tellerrand-2023/bt-poster.webm" type="video/webm"> <source src="https://localghost.dev/img/blog/beyond-tellerrand-2023/bt-poster.mp4" type="video/mp4"> A video showing the stop motion animation of my name with household objects. </video> </div> <p>Between the talks we had DJ sets from the electric <a href="https://baldower.com/">Tobi</a>, incorporating snippets of our talks. It was quite surreal hearing my own voice played back at me as I sat down after I'd spoken!</p> <p>So, that's my speaker review (100/10 would speak again), but I attended the conference too, so here's my attendee review: incredible vibes all the way through. I think it's actually the first multi-day conference where I've watched <em>every single talk</em> - it was that good. Speaking is incredibly exhausting and usually I have to tap out at some point to either go for a walk or chill out somewhere quiet, but the talks were such high quality that I just couldn't miss them. It's quite the honour to have spoken on that roster!</p> <p>There were art talks: meditation and the art of Japanese calligraphy with Aoi Yamaguchi, psychedelic slogans and incredible murals from Gemma O'Brien, algorithmic art and machine learning with Mario Klingemann, stories of toasters and living as a goat from Thomas Thwaites, and light painting with the charming Hugh Elliott. The last talk by Eike König was more of a career retrospective which perhaps went over my head a little, but it was still something different.</p> <div class="content-grid"> <a href="https://localghost.dev/img/blog/beyond-tellerrand-2023/gemma-obrien.JPG" target="_blank"><picture><source type="image/webp" srcset="https://localghost.dev/img/2r6NPnFB2y-280.webp 280w, https://localghost.dev/img/2r6NPnFB2y-640.webp 640w, https://localghost.dev/img/2r6NPnFB2y-960.webp 960w" sizes="auto"><img loading="lazy" decoding="async" src="https://localghost.dev/img/2r6NPnFB2y-280.jpeg" alt="Gemma O'Brien shows the illustrated sick bags she's done on various flights, all with puns relating to movie titles." width="960" height="720" srcset="https://localghost.dev/img/2r6NPnFB2y-280.jpeg 280w, https://localghost.dev/img/2r6NPnFB2y-640.jpeg 640w, https://localghost.dev/img/2r6NPnFB2y-960.jpeg 960w" sizes="auto"></picture></a> <a href="https://localghost.dev/img/blog/beyond-tellerrand-2023/thomas-thwaites.JPG" target="_blank"><picture><source type="image/webp" srcset="https://localghost.dev/img/l8an2Ggr3n-280.webp 280w, https://localghost.dev/img/l8an2Ggr3n-640.webp 640w, https://localghost.dev/img/l8an2Ggr3n-960.webp 960w" sizes="auto"><img loading="lazy" decoding="async" src="https://localghost.dev/img/l8an2Ggr3n-280.jpeg" alt="Thomas Thwaites shows his handmade toaster, which he snuck onto the shelf of John Lewis" width="960" height="720" srcset="https://localghost.dev/img/l8an2Ggr3n-280.jpeg 280w, https://localghost.dev/img/l8an2Ggr3n-640.jpeg 640w, https://localghost.dev/img/l8an2Ggr3n-960.jpeg 960w" sizes="auto"></picture></a> </div> <p>Opening the second day was Dr Emily Anhalt, a clinical psychologist, sharing her tips for being an &quot;emotionally fit&quot; leader - at times what she was talking about resonated so much with me that I got a bit emotional. I'm definitely taking some of her tips away with me to use with my teams.</p> <figure> <a href="https://localghost.dev/img/blog/beyond-tellerrand-2023/emily.JPG" target="_blank"><picture><source type="image/webp" srcset="https://localghost.dev/img/nK-4aKgt-i-280.webp 280w, https://localghost.dev/img/nK-4aKgt-i-640.webp 640w, https://localghost.dev/img/nK-4aKgt-i-960.webp 960w" sizes="auto"><img loading="lazy" decoding="async" src="https://localghost.dev/img/nK-4aKgt-i-280.jpeg" alt="Emily Anhalt stands in front of a slide titled 'Emotional Fitness Survey Questions'. 'Do you like to be praised in public or in private? How do you like to receive feedback? How do you like to be cared for or cheered up during a tough time? How would I know if you were feeling overwhelmed? How do you like your birthday to be celebrated?'" width="960" height="720" srcset="https://localghost.dev/img/nK-4aKgt-i-280.jpeg 280w, https://localghost.dev/img/nK-4aKgt-i-640.jpeg 640w, https://localghost.dev/img/nK-4aKgt-i-960.jpeg 960w" sizes="auto"></picture></a> <figcaption>Emily Anhalt's list of questions that all teams should answer to work together better.</figcaption> </figure> <p>Then we had some excellent typography-related talks from Scott Kellum and Tobias Kunisch, showcasing the importance of dynamic typography on different size screens, and the power of variable fonts. I actually use variable fonts a bit on this site, but I've never really explored their full potential!</p> <figure> <picture><source type="image/webp" srcset="https://localghost.dev/img/RDVV9hnrch-280.webp 280w, https://localghost.dev/img/RDVV9hnrch-640.webp 640w" sizes="auto"><img loading="lazy" decoding="async" src="https://localghost.dev/img/RDVV9hnrch-280.jpeg" alt="Tobias Kunisch talking about variable weight fonts, and the slides just say 'aaaaaaaaaa'" width="640" height="480" srcset="https://localghost.dev/img/RDVV9hnrch-280.jpeg 280w, https://localghost.dev/img/RDVV9hnrch-640.jpeg 640w" sizes="auto"></picture> <figcaption>Same.</figcaption> </figure> <p>Joining me in the web dev contingent were my pals Cassie Evans and Michelle Barker, with some brilliant talks on UI animation and modern CSS layout respectively. I get so excited when I see talks like theirs because they remind me what the web is capable of (and that it's really fun!). Michelle is speaking about Modern CSS Layout at All Day Hey this week, I'll be there!</p> <figure> <picture><source type="image/webp" srcset="https://localghost.dev/img/3A7M_gCDyL-280.webp 280w, https://localghost.dev/img/3A7M_gCDyL-640.webp 640w" sizes="auto"><img loading="lazy" decoding="async" src="https://localghost.dev/img/3A7M_gCDyL-280.jpeg" alt="Cassie Evans stands in front of a slide that says 'The web is an infinite and unknowable canvas' - Miriam Suzanne" width="640" height="480" srcset="https://localghost.dev/img/3A7M_gCDyL-280.jpeg 280w, https://localghost.dev/img/3A7M_gCDyL-640.jpeg 640w" sizes="auto"></picture> <figcaption>Cassie shares a lovely quote from Miriam Suzanne</figcaption> </figure> <p>All in all, I came away feeling so inspired (and so very, very tired). I can't wait to go back next year.</p> <picture><source type="image/webp" srcset="https://localghost.dev/img/pDxJkuo3dE-280.webp 280w, https://localghost.dev/img/pDxJkuo3dE-640.webp 640w, https://localghost.dev/img/pDxJkuo3dE-960.webp 960w" sizes="auto"><img loading="lazy" decoding="async" src="https://localghost.dev/img/pDxJkuo3dE-280.jpeg" alt="A mug with a line drawing of a spaceman on it, next to two Polaroid-style instant photos of me on stage at Beyond Tellerrand, and me at the breakfast table with two other speakers. " width="960" height="720" srcset="https://localghost.dev/img/pDxJkuo3dE-280.jpeg 280w, https://localghost.dev/img/pDxJkuo3dE-640.jpeg 640w, https://localghost.dev/img/pDxJkuo3dE-960.jpeg 960w" sizes="auto"></picture> Painting the whole beetle: an adventure in learning to learn 2023-02-18T00:00:00Z https://localghost.dev/blog/painting-the-whole-beetle-an-adventure-in-learning-to-learn/ <p>I'm not very good at being bad at things. In fact, I have a track record of giving up on things if I'm not immediately good at it. (So I guess I'm good at giving up on things?)</p> <p>Case in point: the piano. There's an electric piano gathering dust on a shelf in my house which was until recently gathering dust on a stand in the living room. A few years ago I started having lessons with a view to being able to accompany myself/my choir, but it was such a challenge to work through being bad at it. I'm a very musical person but my tiny hands made it very difficult to reach some of the intervals, and, well, I just didn't put in the practise because it was so frustrating not being automatically good at it. Of course if I'd persevered, I would have been really good by now, but that's not always how it works in reality.</p> <p>A lot of this comes from my teenage years: I went to a very competitive school where getting a B was tantamount to a failure and I grew up thinking I was average at everything. It wasn't until I left that school and went to a local college for an extra year that I realised that I was actually <em>good at stuff</em>, and my attitude towards education changed considerably (and I worked hard and did well at both college and university).</p> <p>(&quot;Fun&quot; fact: I only went to that college in the first place to study English Language A-Level, because my school didn't offer it – I was told &quot;the kind of universities our girls like to apply to don't see it as a proper subject&quot;. ROTATING YIKES EMOJI.)</p> <p>As a result, I've tended to stick to things I know I'm good at. Singing. Web dev. Cooking and baking. But when something goes wrong – when the cake I'm making for my gran's birthday doesn't rise properly and it's all dense and underbaked true story – I'll have a meltdown and feel like a complete failure.</p> <p>So instead of doing something I might be bad at and feeling frustrated, I'm much more likely to camp out on the sofa and play video games, which I can control the difficulty of if it gets too hard.</p> <p>It's been an uphill struggle to try and unlearn this mindset; to allow myself to be shit at something for a while until I'm good at it. I'm always reminded of the quote from Jake the Dog in Adventure Time:</p> <blockquote> <p>“Dude, suckin’ at something is the first step to being sorta good at something.”</p> </blockquote> <p>He's not wrong, but also I struggle to internalise that because of the years of schooling that told me if I'm even <em>decent</em> at something, there's 20 other people in my class who are a LOT better than I am, and who win the awards at the end of the year.</p> <p>In a fit of uncharacteristic optimism I booked myself onto a one-day &quot;<a href="https://www.city-academy.com/how-to-paint-beginners">How to paint</a>&quot; class. It always struck me as something I'd enjoy doing: the physical act of painting is really relaxing. I turned up to this class with a determination to lower my expectations of myself, and had the best time. We spent the morning on watercolours, which I had basically no experience with, and then in the afternoon moved on to acrylics. The extent of my experience with acrylics had been a cheap paint set I was sent as part of a team social where I painted a creepy looking Monzo card mascot in the same pose as the HA HA! BUSINESS meme, but I do remember it being very satisfying.</p> <p>I had a good time with the watercolours but when we moved onto the acrylics I was struggling a bit. At the teacher's suggestion I'd picked quite a difficult reference image: a purpley-green iridescent beetle.</p> <p>I got about 30 minutes in, and was really struggling to get the colours right, and the highlights were looking really muddy. The leg was pointing out at the wrong angle, and the places where I hadn't applied enough paint looked really rough. I said to the teacher that I thought I might have to give up on this and start a new one but she urged me to keep going with it, and see where it took me.</p> <p>Two hours later, I had finished my beetle, and it was better than anything I'd ever painted before.</p> <figure><picture><source type="image/webp" srcset="https://localghost.dev/img/d5AN2mfBpO-280.webp 280w, https://localghost.dev/img/d5AN2mfBpO-640.webp 640w, https://localghost.dev/img/d5AN2mfBpO-960.webp 960w" sizes="auto"><img loading="lazy" decoding="async" src="https://localghost.dev/img/d5AN2mfBpO-280.jpeg" alt="A photograph of a paint-covered board with a printed reference picture of two iridescent beetles clipped to it. The top beetle is a bright gold-green mix, and the bottom beetle is a purple and blue mix. Underneath the reference photo, a piece of paper is taped to the board with a painting of the bottom beetle. It is slightly messy, but the colours are accurate and the blending is quite good." width="960" height="1280" srcset="https://localghost.dev/img/d5AN2mfBpO-280.jpeg 280w, https://localghost.dev/img/d5AN2mfBpO-640.jpeg 640w, https://localghost.dev/img/d5AN2mfBpO-960.jpeg 960w" sizes="auto"></picture></figure> <p>For me, painting the whole beetle was the first time in a long time I hadn't given up when things started to get difficult and out of my comfort zone. I persevered. And amazingly, I discovered I was Quite Good. Especially at blending colours together. The finer detail was difficult and I didn't use enough paint, but the technique was there.</p> <p>And it occurred to me: I've been painting my own face for 20 years, of course I'm quite good at it.</p> <p>I don't do it nearly as much these days, but in my late teens to my late twenties I'd do the most incredible colourful eyeshadow looks. I even had a makeup blog briefly, though I didn't persevere with that either! But this is it, this is me putting the time in to learn some base skills that have led to me being able to paint a bit.</p> <figure><picture><source type="image/webp" srcset="https://localghost.dev/img/jliKEnOK2i-280.webp 280w, https://localghost.dev/img/jliKEnOK2i-515.webp 515w" sizes="auto"><img loading="lazy" decoding="async" src="https://localghost.dev/img/jliKEnOK2i-280.jpeg" alt="A photo of me aged 23 with very colourful eyeshadow in shades of purple, blue and green, looking into the camera at an angle." width="515" height="478" srcset="https://localghost.dev/img/jliKEnOK2i-280.jpeg 280w, https://localghost.dev/img/jliKEnOK2i-515.jpeg 515w" sizes="auto"></picture><figcaption>Me, circa 2012, doing my best Makeup Blogger face. Serious eyeshadow skills though.</figcaption></figure> <p>I wasn't always good at it. I can think of numerous occasions where I left the house as a teenager looking like someone had put shoe polish on my binoculars. I've made some questionable makeup choices over the years and the execution has not always been <em>on point</em>, shall we say. But all of this is practise and the blending skills I gained from years of doing my makeup in my bedroom when I was bored have clearly stuck with me.</p> <p>At the time it didn't <em>feel</em> like an active process of learning, much like when I was first building websites around the age of 10/11. It was a fun thing to do and I didn't really care about the outcome. I remember struggling to understand why my pictures weren't showing up on the page I'd made when I put it on GeoCities, even though I'd added a nice <code>&lt;IMG SRC=&quot;C:/Sophie/image.jpeg&quot;&gt;</code> tag.</p> <p>And then 14 years later when I started learning Java I marvelled about how naturally coding came to me. When I'd been building websites for 14 years. I'd put in the time! I just couldn't see it!</p> <p>Realistically I know there are very few things one can be naturally good at. Singing is one of those things, but also people who &quot;can't sing&quot; can learn, and even if you've got a naturally good voice it's important to learn how to use it properly so as not to strain it, learn breathing techniques, learn how to sing from your diaphragm etc. People talk a lot about how talented some folks are, but is it really talent or is it <em>skill</em>, honed through years and years of hard work?</p> <p>It's also very possible to have an <em>aptitude</em> for something, but you've still got to put in the time to learn it. I have an aptitude for languages and always did well in German and Spanish at school/uni, but I'm not anywhere near fluent in any languages apart from English, because I haven't put in the time.</p> <p>I'm certainly not naturally good at makeup, or baking, or web development. All of these took a lot of time and practise to learn. And I still screw up a lot because you never really stop learning. My eyeliner may look great but you should see the pile of cotton buds I needed to tidy it up.</p> <p>I spent a bit of time this morning trying out the watercolour paints I'd bought and it took a lot of convincing myself to keep going and not quit as soon as a line wasn't neat enough or the paint was blooming (a bit like a tide mark). But I kept going, and I painted an apple, and it's good for a beginner, and I'm happy that I did it.</p> <figure><picture><source type="image/webp" srcset="https://localghost.dev/img/IowkJiBWi3-280.webp 280w, https://localghost.dev/img/IowkJiBWi3-640.webp 640w, https://localghost.dev/img/IowkJiBWi3-960.webp 960w" sizes="auto"><img loading="lazy" decoding="async" src="https://localghost.dev/img/IowkJiBWi3-280.jpeg" alt="A rough watercolour painting of a red apple which has green bits at the top." width="960" height="1280" srcset="https://localghost.dev/img/IowkJiBWi3-280.jpeg 280w, https://localghost.dev/img/IowkJiBWi3-640.jpeg 640w, https://localghost.dev/img/IowkJiBWi3-960.jpeg 960w" sizes="auto"></picture></figure> <p>This post is not a metaphor; I'm not using this painting experience to dish out some life-changing career advice. For me it's literally about painting. But I know some of you out there will have similar experiences to me, so I guess I wanted to dish out a bit of solidarity and tell you that sometimes with enough determination you can paint the whole beetle too.</p> Everything should have an API: adventures in trying to automate stuff 2023-01-24T00:00:00Z https://localghost.dev/blog/everything-should-have-an-api-adventures-in-trying-to-automate-stuff/ <p>Inspired by <a href="https://rknight.me/automating-my-now-page/">Robb Knight</a> I want to build my own <a href="https://nownownow.com/about">/now page</a>. As a teen I used to use <a href="https://web.archive.org/web/20040603160236/http://www.codegrrl.com/scripts/phpcurrently/index.php">PHPCurrently</a> on my personal website to list what I was listening to, thinking, feeling, even what my MSN display picture was. Here's an objectively terrible screenshot from peak Evanescence phase, circa 2004.</p> <figure> <picture><source type="image/webp" srcset="https://localghost.dev/img/zLFP9V2JuF-280.webp 280w, https://localghost.dev/img/zLFP9V2JuF-601.webp 601w" sizes="auto"><img loading="lazy" decoding="async" src="https://localghost.dev/img/zLFP9V2JuF-280.jpeg" class="small" alt="A screenshot from an old website. It's a list of statistics about what I'm doing. It says 'Currently: MSN display picture: Amy in a pink dress, with lyrics from Missing. date: 11th June. thinking: no more exams!!!! wearing: bathrobe. makeup: none. jewellery: none. hair: loose. MSN screenname: Grammar Nazi. time: 11:30. feeling: amused. eating: raisin wheats. drinking: nothing. surfing: this thread on AGF. you may need this (link) for some of it. IMing: no-one. hating: spelling, grammar and punctuation ignorance. Powered by PHPCurrently." width="601" height="946" srcset="https://localghost.dev/img/zLFP9V2JuF-280.jpeg 280w, https://localghost.dev/img/zLFP9V2JuF-601.jpeg 601w" sizes="auto"></picture> <figcaption> I have grown up a bit since then, thankfully. </figcaption> </figure> <p>And just as Robb did, I want to automate as much of it as I possibly could. No matter how many apps I try for tracking books, games, TV etc., I always forget to actually update them. Everything I do requires a sign-in these days, and it's all internet based, so why shouldn't I be able to automatically generate a page based on the data these companies have on me?<br> In short: they don't let you access it. Netflix, Nintendo, Kobo, PSN to an extent: there's no simple way of getting your history out of these apps. And most of the third-party apps that let you track your history, like GameTrack, have no way out.</p> <p>At the time of writing this I haven't cracked it, but I'll share my findings so far because I'm sure I won't be the only one who wants to do this.</p> <p>If anyone has any suggestions, let me know via Mastodon or email!</p> <h2 id="games" tabindex="-1">Games</h2> <p>Steam has an API because it was historically for nerds (source: am nerd), but I also play a lot of PlayStation and Nintendo Switch.</p> <p>I tried using <a href="https://rawg.io">rawg.io</a> which connects to your PSN account, but it doesn't have a personalised API (just an IMDB-style one for game info) and scraping didn't work because the whole site is a single-page app so the content isn't there without JavaScript. It also only logs what games you <em>have</em>, not what you've played recently - you have to do that bit manually.</p> <p>Nintendo has no API at all, and no public profile pages, so there's no way of logging what you've been playing that I can find.</p> <p>Robb scrapes his latest trophies from <a href="https://psnprofiles.com">psnprofiles.com</a> as a workaround, but I'd really like date-based data.</p> <h2 id="music" tabindex="-1">Music</h2> <p>This bit is actually pretty easy, even being an Apple Music user. I dusted off my old <a href="http://Last.fm">Last.fm</a> account and downloaded <a href="https://apps.apple.com/gb/app/marvis-pro/id1447768809">Marvis Pro</a>. This is like a layer on top of Apple Music with a better interface and automatic <a href="http://Last.fm">Last.fm</a> scrobbling. (There is an official <a href="http://Last.fm">Last.fm</a> app for iOS which picks up Apple Music plays, but it's got limitations and you have to scrobble semi-manually.)</p> <p>I can then render that data pretty nicely using <a href="https://github.com/rknightuk/api/blob/main/services/lastfm.js">Robb's script</a>.</p> <h2 id="books" tabindex="-1">Books</h2> <p>I stopped using Goodreads in 2020 when I de-Amazonified my life as much as I could, so I haven't been tracking what I'm reading. I have a Kobo eReader, sometimes I buy books from their shop, and I borrow a lot of books via Libby/OverDrive. OverDrive <a href="https://developer.overdrive.com/apis">does apparently have an API</a>, but I'm not sure how easy it'll be for an individual to get access. So manually tracking it is.</p> <h3 id="manually-tracking-books" tabindex="-1">Manually tracking books</h3> <p>Two apps were recommended to me recently: <a href="https://oku.club">Oku</a> and <a href="https://librarything.com">Library Thing</a>. (Side note: I love the two completely different aesthetics of these sites: Oku looks like someone copied the CSS from Notion, and Library Thing transports me straight back to 2003.)</p> <p>Oku has an RSS feed for every user collection, which means I can easily grab the feed for &quot;currently reading&quot;! The feed also contains metadata like cover image, so I'll be able to render the books nicely (though quite a few books appear to be missing covers in their database). Its search is pretty bad, but if you feed it an ISBN it gets the book straight away.</p> <p>LibraryThing has better book data and it does have APIs, but they're disabled right now and it's unclear if they're going to turn them back on again, so that's a dead end (at least for now). So Oku it is. Let's see if I remember to track what I'm reading this time.</p> <h2 id="podcasts" tabindex="-1">Podcasts</h2> <p>My podcast app of choice is <a href="https://pocketcasts.com/">Pocket Casts</a>, and there appears to be no official way of getting listening data out of that. But there does seem to be an API <em>somewhere</em>, as some folks have <a href="https://willschenk.com/articles/2019/reverse_engineering_apis_using_chrome/">reverse engineered it</a>. Authentication looks a bit dodgy (not about to store username and password anywhere) so I'll have to figure out how long that authorization token lasts.</p> <p>If anyone knows a better podcast app for gathering this kind of data, let me know!</p> <h2 id="recipes" tabindex="-1">Recipes</h2> <p>I'd love to log what I've been cooking this week! On the weeks when I have my shit together I plan what we're going to cook using <a href="https://www.paprikaapp.com/">Paprika</a> (where I store all my recipes - about 3000 of them!) - this is ripe for automation. Sadly, the app is pretty much &quot;done&quot; in as much as they don't add new stuff to it any more, and there's no API or anything. I've considered switching to <a href="https://pestlechef.app/">Pestle</a>, but there's no MacOS app yet which is unfortunately a dealbreaker for me. So it might have to be a manual process for the time being.</p> <p>Paprika <em>does</em> have a calendar export option, where it provides a calendar feed of planned meals. I wonder if there's any way of intercepting that.</p> <h2 id="tv-and-films" tabindex="-1">TV &amp; films</h2> <p>Again, plenty of apps here for manually tracking what I'm watching, but no way of getting the data out that I could see. However, in my search today I found <a href="https://trakt.tv/">Trakt</a> which <em>does</em> have a public API!</p> <blockquote> <p>Part of the fun with such data is making it available for anyone to mash up and use on their own site or app. The Trakt API was made just for this purpose. It is very easy to use, you basically call a URL and get some JSON back.</p> </blockquote> <p>That's what I'm talking about! Why bother tracking what you're playing/reading/watching if you can't then do fun stuff with that data? Recommendations are useful enough, but this is the good stuff.</p> <p>The interface is confusing at best, and it's surprisingly fiddly to mark things as watched. It thinks I watched all 9 seasons of Breaking Bad <em>today</em>, as I marked them as watched in one tap.</p> <p>Again, the biggest downside is I have to track manually, but at least that way I don't accidentally reveal to everyone that I'm three seasons in to Emily in Paris. But ultimately I'm not sure if I'll be able to stick it out (the app, not Emily in Paris).</p> <h2 id="conclusion" tabindex="-1">Conclusion</h2> <p>At this point I can only conclude that I've spent longer investigating how I can automate this than I would have done manually updating the /now page once a week. But it's also a matter of remembering/having the energy to update it. I can guarantee you I'd update it twice and then leave it for several years.</p> <p>It frustrates me how easy this would be if all these services had APIs that you could just pull your own data from. If our lives are online now, and our data is helping them personalise their services and make more money through recommendations, the least they could do is provide that data in a consumable format. It's not exactly feasible to do a monthly <a href="https://ico.org.uk/for-organisations/guide-to-data-protection/guide-to-the-general-data-protection-regulation-gdpr/individual-rights/right-of-access/"><abbr title="Data Subject Access Request">DSAR</abbr></a> to get my Netflix viewing history.</p> I miss Twitter 2023-01-14T00:00:00Z https://localghost.dev/blog/i-miss-twitter/ <p>I'm still sad about Twitter.</p> <p>I think what has emerged for me since switching to Mastodon is an altogether healthier relationship with social media. I'm not checking it constantly and I'm not obsessing over how my tweets are doing. I can post things without worrying that idiots have saved searches for key words/names and are going to come and shit all over the replies.</p> <p>And it feels pathetic to be mourning the loss of a social media site. We know social media is Bad and the algorithm is Bad and it's all just late stage capitalism Bad Bad Bad. And there are plenty of people on Mastodon celebrating its demise.</p> <p>But I miss it. I miss the vibes of tech Twitter, I miss the community feel that I just haven't felt on Mastodon in the same way. I'm worried that not having a Twitter presence is going to affect my speaking career and mean I don't get as many conference invitations. I'm angry that one billionaire man-child was able to take this community away from us.</p> <p>There are still quite a few people sticking around Twitter as it clings on for dear life but I just can't bring myself to go back to a site that's unbanned some really high-profile transphobes, bigots, misogynists and racists. And a literal human trafficker, it turns out.</p> <h3 id="mastodon-is-fine-i-guess" tabindex="-1">Mastodon is fine I guess</h3> <p>While it's been a <em>generally</em> positive experience so far, IMO Mastodon isn't quite the paradise that people have made it out to be: as a platform (decentralised though it may be) that was relatively tiny for a long time before suddenly blowing up recently, I've encountered some users on there who resent that and think that if you're new, you shouldn't have an opinion. In a way I get it: Twitter's demise was the start of Mastodon's <a href="https://en.wikipedia.org/wiki/Eternal_September">Eternal September</a>. But let's not kid ourselves: Mastodon still has mansplainers, it has racists, it has arseholes. It's just harder for them to find your posts.</p> <p>I saw one post about how people should put politics-related content under content warnings (CWs), and when I disagreed (because politics is people's lived experience) the author told me that I'd only had my account a few weeks and that I should listen to the people who had been there for years. And that anything about Black people being silenced/experiencing racism on Mastodon had been &quot;proven to not be true&quot;. They provided no evidence to back this up - I know, I'm as shocked as you are.</p> <p>Notably, in the two months I've been on Mastodon my feed has remained predominantly male and white, despite trying to diversify my follows. A lot of people I followed initially have returned to Twitter as their other communities (e.g. Black Twitter, trans Twitter) have remained there.</p> <h3 id="shouting-into-the-void" tabindex="-1">Shouting into the void</h3> <p>I miss the reach I used to have on Twitter. Talking about issues I cared about, like semantic HTML and mental health, and knowing that lots of people would see it (even if generally I got more engagement on the shitposts).</p> <p>Glad as I am that the personal website is having a renaissance – you know how much I love personal websites – for me that fulfils a different function from sites like Twitter. This site is my own space on the internet, but it's me shouting into the void in a way that Twitter wasn't.</p> <p>Mostly I'm just sad that we lost the community that we had. And I hope we can rebuild it somehow.</p> 2022: The year in lists 2022-12-30T00:00:00Z https://localghost.dev/blog/2022-the-year-in-lists/ <p>I don't usually do these end-of-year reflection posts, but at a time where I feel like I'm finally starting to hit flat land again after a year of climbing hills, it seems like a nice thing to do, and a way for me to reflect on my own achievements.</p> <p>I've spent a <em>lot</em> of time mulling over the bad things that happened this year so I want to focus on the good stuff.</p> <p>For that reason, there won't be much mention of COVID. I want to make it perfectly clear that I see it as very much still a thing, I got it for the first time this autumn and it completely wiped me out. Now that the post-COVID immunity has worn off somewhat, I'll once again be wearing masks in crowded places and on public transport well into 2023.</p> <p>Skip to bits you care about:</p> <ul> <li><a href="https://localghost.dev/blog/2022-the-year-in-lists/#the-year-in">The year in...</a> <ul> <li><a href="https://localghost.dev/blog/2022-the-year-in-lists/#career-decisions">...career decisions</a></li> <li><a href="https://localghost.dev/blog/2022-the-year-in-lists/#conferences">...conferences</a></li> <li><a href="https://localghost.dev/blog/2022-the-year-in-lists/#travel">...travel</a></li> <li><a href="https://localghost.dev/blog/2022-the-year-in-lists/#books">...books</a></li> <li><a href="https://localghost.dev/blog/2022-the-year-in-lists/#music">...music</a></li> <li><a href="https://localghost.dev/blog/2022-the-year-in-lists/#video-games">...video games</a></li> <li><a href="https://localghost.dev/blog/2022-the-year-in-lists/#learning-things">...learning things</a></li> <li><a href="https://localghost.dev/blog/2022-the-year-in-lists/#blog-posts">...blog posts</a></li> <li><a href="https://localghost.dev/blog/2022-the-year-in-lists/#the-web">...the web</a></li> </ul> </li> </ul> <h2 id="the-year-in" tabindex="-1">The year in...</h2> <h3 id="career-decisions" tabindex="-1">...career decisions</h3> <p>Last year was the year I left my job at Monzo and joined a startup, and this year I <a href="https://localghost.dev/blog/when-going-back-doesn-t-mean-going-backwards/">did the opposite</a>. I made some tough decisions and got really burned out, but coming back to Monzo in August was amazing and has been just what I needed to help me understand more about what I want out of my career. Thankfully, I've recovered from the burnout I was suffering from earlier this year.</p> <h3 id="conferences" tabindex="-1">...conferences</h3> <p>This year I found myself speaking at <em>seven</em> conferences... and attending one!</p> <ul> <li>In <strong>March</strong>, I showed <a href="https://cityjsconf.org/">CityJS London</a> how to build a virtual &quot;piano&quot; with the Web Audio API.</li> <li>In <strong>April</strong>, attending <a href="https://heypresents.com">All Day Hey</a> in Leeds made me fall in love with the web all over again.</li> <li>In <strong>May</strong> I took my virtual piano talk to my first international conference, <a href="https://www.bejs.io/conf">BeJS</a> in Brussels!</li> <li>In <strong>June</strong> I spoke at <a href="https://2022.uxlondon.com/speakers/sophie-koonin/">UX London</a> about the importance of clear writing in product development, based on a <a href="https://incident.io/blog/use-your-words-the-importance-of-clear-writing-product-development">blog post I'd written for incident.io</a>.</li> <li>In <strong>July</strong> I opened <a href="https://skillsmatter.com/conferences/13770-fullstack-exchange-2022#overview">FullStack eXchange</a> in London with a keynote about the <a href="https://skillsmatter.com/skillscasts/17629-sophie-koonin">history of personal websites</a>. My laptop completely crashed at the beginning of the talk, so I had to restart my computer while everyone watched...!</li> <li>In <strong>October</strong>, from the glorious Barbican Centre in London, I showed the lovely folks of <a href="https://2022.stateofthebrowser.com/">State of the Browser 2022</a> how to <a href="https://localghost.dev/blog/building-a-website-like-it-s-1999-in-2022/">build a website like it's 1999</a>.</li> <li>In <strong>November</strong> I achieved a career goal of speaking at <a href="https://ffconf.org/">FFConf</a> in Brighton, delivering a <a href="https://www.youtube.com/watch?v=vGYm9VdfJ8s&amp;list=PLZy5V2JKDfX9afwuEl1NolNpvd0yNWc8E&amp;index=5">love letter to the personal website</a>. It was everything I'd hoped for, and I'm so proud of it. If you've never been, it's one of the loveliest communities around.</li> <li>And finally in <strong>December</strong> – the week before Christmas! – I travelled to Málaga in Spain for the inaugural <a href="https://weyweyweb.com">WeyWeyWeb</a> to play that virtual piano again, <a href="https://youtu.be/YkKYuQBjmtA">this time with added Web MIDI</a>. I very nearly didn't make it thanks to a combination of train strikes and snow, but I'm glad I did!</li> </ul> <p>At times I was a bit overwhelmed with conference prep, but ultimately I had a great time, and I can't wait for the conferences that 2023 will bring. (my inbox is always open!)</p> <h3 id="travel" tabindex="-1">...travel</h3> <p>International travel picked up again this year, with trips to Brussels and Spain for conferences, and Scotland, France and Iceland for holidays.</p> <p>Iceland was absolutely incredible, I really can't recommend it enough. A week of driving around, taking photos and being surrounded by the most incredible landscapes I've ever seen.</p> <figure> <a href="https://localghost.dev/img/blog/2022-recap/skogafoss.webp"> <picture><source type="image/webp" srcset="https://localghost.dev/img/CpcYYN7UsG-280.webp 280w, https://localghost.dev/img/CpcYYN7UsG-640.webp 640w, https://localghost.dev/img/CpcYYN7UsG-960.webp 960w" sizes="auto"><img loading="lazy" decoding="async" src="https://localghost.dev/img/CpcYYN7UsG-280.jpeg" alt="A photo of Skógafoss, a waterfall in Iceland. The waterfall is wide, set between two moss-covered rocks. There is a lot of spray in the air and the weather is overcast." width="960" height="640" srcset="https://localghost.dev/img/CpcYYN7UsG-280.jpeg 280w, https://localghost.dev/img/CpcYYN7UsG-640.jpeg 640w, https://localghost.dev/img/CpcYYN7UsG-960.jpeg 960w" sizes="auto"></picture> </a> <figcaption>Skógafoss, Iceland </figcaption></figure> <figure> <picture><source type="image/webp" srcset="https://localghost.dev/img/DRnz8KtwUk-280.webp 280w, https://localghost.dev/img/DRnz8KtwUk-640.webp 640w, https://localghost.dev/img/DRnz8KtwUk-960.webp 960w" sizes="auto"><img loading="lazy" decoding="async" src="https://localghost.dev/img/DRnz8KtwUk-280.jpeg" alt="A bright orange sunset, looking out onto the water with some very dark stretches of beach at the bottom, so dark they are black." width="960" height="640" srcset="https://localghost.dev/img/DRnz8KtwUk-280.jpeg 280w, https://localghost.dev/img/DRnz8KtwUk-640.jpeg 640w, https://localghost.dev/img/DRnz8KtwUk-960.jpeg 960w" sizes="auto"></picture> <figcaption>Sunset at Akranes </figcaption></figure> <p>I finally got the Eurostar (to Brussels) for the first time since it's been at St Pancras and honestly I now see why everyone's so obsessed with it. It's SO GOOD.</p> <p>I also spent some time in Suffolk with friends, and we went birdwatching at RSBP Minsmere, where we saw an amazing murmuration of starlings!</p> <figure> <picture><source type="image/webp" srcset="https://localghost.dev/img/Nk-XKnWIPt-280.webp 280w, https://localghost.dev/img/Nk-XKnWIPt-640.webp 640w, https://localghost.dev/img/Nk-XKnWIPt-960.webp 960w" sizes="auto"><img loading="lazy" decoding="async" src="https://localghost.dev/img/Nk-XKnWIPt-280.jpeg" alt="A flock of starlings moving together in a mushroom cloud shape, above a marsh. The starlings look like little black specks, almost like insects. There are larger gulls flying around." width="960" height="1280" srcset="https://localghost.dev/img/Nk-XKnWIPt-280.jpeg 280w, https://localghost.dev/img/Nk-XKnWIPt-640.jpeg 640w, https://localghost.dev/img/Nk-XKnWIPt-960.jpeg 960w" sizes="auto"></picture> <figcaption>Murmuration at RSPB Minsmere </figcaption></figure> <h3 id="books" tabindex="-1">...books</h3> <p>Last year in an attempt to de-Amazon my life a bit I swapped out my Kindle for a <a href="https://uk.kobobooks.com/products/kobo-libra-h2o">Kobo Libra</a>, which has built-in Overdrive support. So I've been enjoying renting ebooks from various local libraries!</p> <p>A few of the novels I particularly enjoyed this year:</p> <ul> <li><a href="https://uk.bookshop.org/books/the-four-winds-the-number-one-bestselling-richard-judy-book-club-pick/9781529054583"><em>The Four Winds</em></a> by Kristin Hannah</li> <li><a href="https://uk.bookshop.org/books/seven-husbands-of-evelyn-hugo-the-sunday-times-bestseller/9781398515697"><em>The Seven Husbands of Evelyn Hugo</em></a> by Taylor Jenkins Reid</li> <li><a href="https://uk.bookshop.org/books/going-postal-discworld-novel-33/9780552167680"><em>Going Postal</em></a> by Terry Pratchett</li> <li><a href="https://uk.bookshop.org/books/the-book-thief/9781909531611"><em>The Book Thief</em></a> by Markus Zusak</li> <li><a href="https://uk.bookshop.org/books/if-i-had-your-face-assured-bold-and-electrifying-taylor-jenkins-reid-bestselling-author-of-malibu-rising-9780241986356/9780241986356"><em>If I Had Your Face</em></a> by Frances Cha</li> <li><a href="https://uk.bookshop.org/books/the-dictionary-of-lost-words-a-reese-witherspoon-book-club-pick/9781529113228"><em>The Dictionary of Lost Words</em></a> by Pip Williams</li> <li><a href="https://uk.bookshop.org/books/the-song-of-achilles-9781408891384/9781408891384"><em>The Song of Achilles</em></a> by Madeleine Miller</li> <li><a href="https://uk.bookshop.org/books/how-beautiful-we-were-a-novel-9781838851378/9781838851378"><em>How Beautiful We Were</em></a> by Imbolo Mbue</li> <li><a href="https://uk.bookshop.org/books/free-food-for-millionaires/9781801105323"><em>Free Food For Millionaires</em></a> by Min Jin Lee</li> </ul> <p>In the last month or so I picked up a copy of <a href="https://www.oreilly.com/library/view/the-staff-engineers/9781098118723/">The Staff Engineer's Path</a> by Tanya Reilly. It's full of really useful advice and scenarios for anyone who's either looking to get to Staff, or understand more about what that role entails. Since it's a work-related book it's not the kind I tend to devour in an evening, so I'm making my way through it VERY slowly, but it's definitely a useful one.</p> <h3 id="music" tabindex="-1">...music</h3> <p>I can't listen to music with lyrics while I work, so it's lo-fi beats to relax and/or debug to all the way, but here are some particularly good albums (new and old) that I enjoyed this year. It seems to be 2003 again with some of the new releases and I'm honestly living for it.</p> <ul> <li><a href="https://carlyraejepsen.lnk.to/TheLoneliestTime">Carly Rae Jepsen – The Loneliest Time</a> Album of the year 1000%. If you still think she only did &quot;Call Me Maybe&quot;, do yourself a favour and listen to her last three albums.</li> <li><a href="https://wetleg.bandcamp.com/album/wet-leg">Wet Leg – Wet Leg</a> Superb mid-00s style indie.</li> <li><a href="https://www.maggielindemann.com/products/suckerpunch-digital-album">Maggie Lindemann – SUCKERPUNCH</a> – Evanescence meets Avril Lavigne meets my teenage self. Makes me feel old, but it's a vibe nonetheless.</li> <li><a href="https://editors.bandcamp.com/album/ebm">Editors – EBM</a></li> <li><a href="https://muna.bandcamp.com/album/muna">MUNA – MUNA</a></li> <li><a href="https://caitlinrose.bandcamp.com/album/cazimi">Caitlin Rose – Cazimi</a> – her 2013 album The Stand-In is one of my all-time faves</li> <li><a href="https://www.lacunacoil.com/project/comalies-xx/">Lacuna Coil – Comalies XX</a> – rearranged and re-recorded versions of the original. The new version of Swamped is <em>everything</em>.</li> </ul> <h3 id="video-games" tabindex="-1">...video games</h3> <p>One thing that didn't change this year is that I played a shit-ton of video games. Here are some highlights.</p> <ul> <li><a href="https://www.playstation.com/en-gb/games/horizon-forbidden-west/">Horizon: Forbidden West</a> – Horizon: Zero Dawn is one of my all-time favourites, and this didn't disappoint! The world-building is incredible, it's absolutely stunning, and I love hunting around for datapoints to discover even more lore.</li> <li><a href="https://tunicgame.com/">Tunic</a> – lovely Zelda-esque game with some REALLY creative puzzles. Really loved this one.</li> <li><a href="https://playwonderlands.2k.com/">Tiny Tina's Wonderlands</a> – based on my favourite Borderlands 2 DLC, Tiny Tina's Assault on Dragon Keep, this was a delightful (and hilarious) D&amp;D-themed action game. Borderlands remains the only FPS-style game I've ever enjoyed. Disappointing DLC that I didn't even bother with, but I got a lot of hours out of this game!</li> <li><a href="https://cult-of-the-lamb.com/">Cult of the Lamb</a> was great fun for a while, but also had some serious bugs that made the game really frustrating (these have been patched now). It also <em>finally</em> got me to like roguelikes, which resulted in me finally playing...</li> <li><a href="https://www.supergiantgames.com/games/hades/">Hades</a>. I'm pretty bad at it, but everything about this game rules.</li> <li><a href="https://bethesda.net/en/game/deathloop">Deathloop</a> was a lot of fun, even if I did enable a LOT of accessibility settings! Fascinating story, great setting, and very clever puzzles.</li> <li><a href="https://www.somethingwemade.se/toem/">TOEM: A photo adventure</a> which was extremely cute and I loved it</li> <li><a href="https://www.nintendo.co.uk/Games/Nintendo-Switch-download-software/Carto-1859331.html">Carto</a> – wonderful little indie game</li> <li><a href="https://chicorygame.com/">Chicory: A Colorful Tale</a> – a lovely allegory for depression and making the world seem more beautiful again.</li> <li><a href="https://stray.game/">Stray</a> – another vote for beautiful world-building and lovely storytelling.</li> <li><a href="https://www.ea.com/en-gb/games/it-takes-two">It Takes Two</a> – hooray, finally a couch co-op game! The story was absolute dogshit and we hated everyone in it, but the gameplay was excellent and made up for it all. Really, really creative co-op.</li> <li>Shamefully I've only just started <a href="https://returntomonkeyisland.com/">Return to Monkey Island</a>, but I'm loving it so far obviously, even if the new art style is a little jarring and I miss being able to make Guybrush say &quot;I'm not picking that up.&quot;</li> <li>I've also spent the last few weeks replaying Skyrim – 11 years later! – and it still holds up. Incredible. You could probably run it on an electric screwdriver at this point. (Still buggy as shit though, even on PS5)</li> </ul> <p>I've noticed that game devs have been adding a lot more accessibility settings to games in the last few years. Accessibility isn't just about being able to play games if you're disabled, it's about <em>everyone being able to enjoy the game</em>. I'm... aggressively okay at video games, and so for games like Hades and Deathloop that have really challenging combat, the only way I can get through it without dying over and over and over again at the same place (and inevitably rage-quitting) is to turn the difficulty down. I don't always want to play on easy mode, though. Lots of games now have more granular accessibility settings, so you can turn down the damage you receive, or make it so that you get a little stronger every time you die. This means I don't feel like I'm walking through the game with no challenge at all, I can still enjoy the story, and play it at a level of challenge that suits me.</p> <h3 id="learning-things" tabindex="-1">...learning things</h3> <p>Some of the things I learned this year:</p> <ul> <li>how to create shadow effects with CSS <code>perspective</code></li> <li>how to use the Web MIDI API to hook up a MIDI controller to the browser</li> <li>basic music production – recording, mixing and mastering</li> <li>about the <a href="https://moderncss.dev/practical-uses-of-css-math-functions-calc-clamp-min-max/#clamp">CSS function <code>clamp()</code></a></li> <li>how to make <a href="https://www.atsukoskitchen.com/japanese-cooking-classes/">okonomiyaki and takoyaki</a></li> </ul> <h3 id="blog-posts" tabindex="-1">...blog posts</h3> <p>I wrote a whopping SIX posts this year! It may not sound that much, but it's a record since I've had this site. I'm hoping I can keep momentum going into next year.</p> <ul> <li><a href="https://localghost.dev/blog/start-at-the-beginning-the-importance-of-learning-the-basics/">Start at the beginning: the importance of learning the basics</a> – frustrated with the number of people advocating for skipping learning HTML/CSS and just doing React/Tailwind, I wrote this article about why that's a terrible idea.</li> <li><a href="https://localghost.dev/blog/burnout-a-cautionary-tale-and-a-plea-to-take-a-break/">Burnout, a cautionary tale (and a plea to take a break)</a> – I'd taken a break from work, as I was in a bad place and completely exhausted.</li> <li><a href="https://localghost.dev/blog/when-going-back-doesn-t-mean-going-backwards/">When going back doesn't mean going backwards</a> – the successor to the previous post. Going back to my old job, and feelings about everything that had happened.</li> <li><a href="https://localghost.dev/blog/everything-i-googled-in-a-week-as-a-senior-software-engineer/">Everything I googled in a week as a senior software engineer</a> – an updated version of the original I wrote in 2019, to show that I still google everything!</li> <li><a href="https://localghost.dev/blog/building-a-website-like-it-s-1999-in-2022/">Building a website like it's 1999, in 2002</a> – the written version of the talk I gave at State of the Browser this year. It went pretty viral a few days ago as someone posted it on Hacker News, which was both cool and terrifying.</li> <li><a href="https://localghost.dev/blog/preparing-for-conferences/">Preparing for conferences</a> – a summary of my process for preparing to give a conference talk, from talk preparation to delivery.</li> </ul> <h3 id="the-web" tabindex="-1">...the web</h3> <ul> <li>I left Twitter for Mastodon, and <a href="https://social.lol/@sophie/109597731355994610">I'm not actually sure I like it more than Twitter</a>. But I don't want to go back to a platform that's unbanned some of the most detestable people in recent history.</li> <li>I joined <a href="https://omg.lol">omg.lol</a>, and you should too. Until 1 Jan 2023, it's $5 a <em>year</em> for a pastebin (paste.lol), URL shortener (url.lol), Mastodon instance (social.lol), email forwarder (@omg.lol) and soon a weblog (weblog.lol)! I now have a fancy <a href="https://sophie.omg.lol/">&quot;web card&quot;</a> I can link people to if they want to get in touch. It's just lovely vibes all round.</li> <li>It feels like there's been a bit of a personal website resurgence, which is lovely – especially considering the topics of the talks I've given this year! I gave this site a bit of a facelift, with five new themes including some very silly ones. I also moved it to <a href="https://neocities.org">neocities</a> and joined some webrings because WHY NOT.</li> </ul> <h2 id="there-you-have-it" tabindex="-1">There you have it <!-- omit in toc --></h2> <p>It's been nice to spend some time reflecting on the good things that happened this year despite all the bad. I'm hoping to keep the conference momentum going, so if you know any that are looking for speakers, let me know! Here's hoping 2023 brings new opportunities and more personal website joy.</p> Preparing for conferences 2022-12-14T00:00:00Z https://localghost.dev/blog/preparing-for-conferences/ <p>I've been speaking at conferences and meetups on and off for nearly five years now, and a few people have asked me what the process is for preparing a talk. So I thought I'd share how I approach it.</p> <p>For me, the talk process is a circle:</p> <figure><picture><source type="image/webp" srcset="https://localghost.dev/img/rdfa7CQA3z-280.webp 280w, https://localghost.dev/img/rdfa7CQA3z-640.webp 640w, https://localghost.dev/img/rdfa7CQA3z-960.webp 960w" sizes="auto"><img loading="lazy" decoding="async" src="https://localghost.dev/img/rdfa7CQA3z-280.jpeg" alt="A circular flow diagram. The top box says &quot;YEAH LET'S DO A TALK!&quot;, which flows to a box that says &quot;Oh no, I have to prepare the talk.&quot;. That box flows to one that says &quot;This is the worst. I hate this. Why did I agree to this?&quot;. The one after that says &quot;Okay! Ready! Let's do this!&quot;, and the box after that says &quot;THAT WAS AMAZING! I want to do it again!&quot;. That box points back round to the first box, in a circle." width="960" height="750" srcset="https://localghost.dev/img/rdfa7CQA3z-280.jpeg 280w, https://localghost.dev/img/rdfa7CQA3z-640.jpeg 640w, https://localghost.dev/img/rdfa7CQA3z-960.jpeg 960w" sizes="auto"></picture><figcaption>It's the circle of talks.</figcaption></figure> <p>It repeats around and around forever. There is <em>always</em> a point at which I hate my talk and wish I'd never done it, but there is also always a point where I'm having the time of my life. Giving talks is great fun, but it's also hard work.</p> <p>(As a sort-of aside: that hard work is why you should never speak at a conference that isn't at <em>least</em> paying for your travel/accommodation or taking you to dinner. Your time is worth money.)</p> <p>I'll break my process down into a set of steps, but the <abbr title="too long; didn't read">tl;dr</abbr> is: <strong>practise</strong>. Your audience deserve to watch a talk that you've rehearsed and aren't just making up on the spot. Not only will it show if you haven't practised, you're much more likely to run over time as well. The practise part isn't optional!</p> <p>The timelines are super rough, but generally I'd start around 2-2.5 months before the talk. I've done talks in shorter timeframes than that, but I wouldn't recommend it – taking longer means I don't have to dedicate all of my free time to it, I can space the work out a lot more.</p> <h2 id="initial-planning-about-8-10-weeks-before-the-conference" tabindex="-1">Initial planning: about 8-10 weeks before the conference</h2> <p>To begin with: identify the overarching theme(s) of the talk. What are the main points I want to convey? No more than 3 or 4.</p> <p>I make short-form notes for content ideas – bullet points or post-it notes – jotting down any ideas that come to mind, and sorting them into themes afterwards.</p> <p>Previously, I've used notecards blue-tacked to the wall with ideas on, so I can shuffle them around and group them.</p> <p>At this point, I'll also do research. Depending on the topic, I might read blog posts, articles, I even read someone's PhD thesis once. I might pull out some good quotes, or just write down some general ideas from those.</p> <h2 id="content-about-6-8-weeks-before" tabindex="-1">Content: about 6-8 weeks before</h2> <p>Taking my notes, and writing more detail for each point in a Notion doc. What do I have to say about each of these ideas? Sometimes I spot how they flow into each other, or I might find that one of them doesn't fit with the others any more after I've written some blurb.</p> <p>I treat it a bit like a blog post at this point, writing as if I'm talking to someone. It helps me to solidify my ideas as a talk rather than just random sentences.</p> <h2 id="slides-about-4-6-weeks-before" tabindex="-1">Slides: about 4-6 weeks before</h2> <p>Depending on my needs, I'll either use Keynote or Google Slides. I prefer Keynote, but if I need to be able to switch to a browser quickly, Google Slides is better. I used Reveal once which was useful for code and embeds, but it took me ages to build the slides so I probably won't use it very often.</p> <p>The first version of the slides always has the notes containing the full sentences from the content planning stage, because I'll use these for the first few run-throughs.</p> <p>If my talk needs any code snippets, I'll screenshot them from VSCode so they look pretty (or, if I'm using Reveal, I'll put them straight in the slides).</p> <p>This part is also about editing, editing, editing. Especially for shorter talks, it's as much about what you leave out as it is what you put in.</p> <p>Not gonna lie: at this point I start to hate my talk. I'll come around to it again, but right now I'll be fed up with it and full of regret. It's really hard work, and I find making slides pretty tedious. Prepare to have some moments like this – it will get better, though!</p> <h2 id="run-throughs-from-3-4-weeks-before" tabindex="-1">Run-throughs: from 3-4 weeks before</h2> <p>Early on, I'll run my slides through with my husband who's usually pretty good at spotting things that don't fit, or things I can leave out. I always recommend doing a run-through with a partner, friend or colleague!</p> <p>I sometimes run talks at work as well – we have a weekly slot for engineering talks that anyone can give. If you don't have that, perhaps you could introduce it at your company?</p> <p>I'll do three or four run-throughs a week, after work, where I shut myself in our little office and just deliver the talk to nobody. When we have a dog staying, it's a bit easier as I can give the talk to him. (He's never particularly interested in what I have to say, though.)</p> <h2 id="the-week-of-the-conference" tabindex="-1">The week of the conference</h2> <p>I don't tend to practise my talk in the days leading up to the conference. Often it's because I'm travelling or busy in the evening with a speakers' dinner. But I also think it'd just stress me out. By the time the conference arrives, I've practised it enough that I feel happy with it.</p> <h2 id="delivering-the-talk" tabindex="-1">Delivering the talk</h2> <p>I'm not going to go into a huge amount of detail here – the <a href="https://localghost.dev/blog/things-experienced-speakers-wish-they-d-known">post I wrote when I was preparing for my first conference in 2018</a> covers a lot of that ground – but the main thing I've learned is that I get super nervous as I'm waiting to go on stage, and then when I get on stage it just goes away and I have a great time.</p> <p>If you've practised enough, you shouldn't need to stare at your speaker notes – just glance at them for a prompt. Sometimes you'll find you won't need to look at them at all.</p> <p>Afterwards, you'll probably feel amazing and want to do more talks, and thus the cycle starts again. (Conference organisers: I'm at my most suggestible in the week after I've just given a talk.)</p> Building a website like it's 1999... in 2022 2022-10-23T00:00:00Z https://localghost.dev/blog/building-a-website-like-its-1999-in-2022/ <p><strong>Edit May 2023:</strong> the Bridgy integration with Twitter no longer works due to Twitter shutting off API access so some functionality won't be working any more!</p> <p><em>Note: This is a written version of a talk I gave at State of the Browser 2022 in October 2022!</em></p> <p><strong>Motion warning</strong>: This page contains quite a few animations, but if you have reduced motion turned on, they won't play.</p> <h2 id="the-web-used-to-be-weirder" tabindex="-1">The web used to be weirder</h2> <p>I'm on a bit of a mission this year to bring back the spirit of the old web. The creativity and flair of the late 90s and early 2000s. Back then, there were no rules – you put whatever you wanted on a webpage, because it was your space to do as you please.</p> <p>And for a whole generation of internet users, having a website was the cool thing to do. It's just what you did back then. We're talking pre-social media, pre-web 2.0 – the good old fashioned static personal home page.</p> <p>Sites like Geocities, Angelfire, Tripod and Expage offered free static hosting for all, and the number of personal websites boomed. Some hosts offered drag-and-drop website builders so you didn't even have to learn HTML.</p> <p>We might look back on these websites now and laugh – they look ridiculous compared to the sleek and minimalist sites we're used to nowadays. But I actually think we've gone too far in the other direction, and now so many websites look the same. These old personal websites were a reflection of yourself.</p> <p>Some of these websites were for family to share photos and updates...</p> <figure> <picture><source type="image/webp" srcset="https://localghost.dev/img/JjIz4h52gG-280.webp 280w, https://localghost.dev/img/JjIz4h52gG-640.webp 640w, https://localghost.dev/img/JjIz4h52gG-960.webp 960w" sizes="auto"><img loading="lazy" decoding="async" src="https://localghost.dev/img/JjIz4h52gG-280.jpeg" alt="A brightly coloured website that says 'Welcome to Tom & Sherry's Proud Grandparents page. The Proud Grandparents page was created to show pictures of our grandchildren to family and friends, and an occasional Web surfer. The grandkids, our pride and joy, and their parents have made us very proud. Okay, let's see the pictures!'" width="960" height="713" srcset="https://localghost.dev/img/JjIz4h52gG-280.jpeg 280w, https://localghost.dev/img/JjIz4h52gG-640.jpeg 640w, https://localghost.dev/img/JjIz4h52gG-960.jpeg 960w" sizes="auto"></picture> <figcaption><a href="https://geocities.restorativland.org/Heartland/Ridge/1217/">The Proud Grandparents Page</a> </figcaption></figure> <p>...while others were full of graphics to share and use on your own site...</p> <figure> <picture><source type="image/webp" srcset="https://localghost.dev/img/EvB4_PHQpq-280.webp 280w, https://localghost.dev/img/EvB4_PHQpq-640.webp 640w, https://localghost.dev/img/EvB4_PHQpq-960.webp 960w" sizes="auto"><img loading="lazy" decoding="async" src="https://localghost.dev/img/EvB4_PHQpq-280.jpeg" alt="Lisa's graphics" width="960" height="713" srcset="https://localghost.dev/img/EvB4_PHQpq-280.jpeg 280w, https://localghost.dev/img/EvB4_PHQpq-640.jpeg 640w, https://localghost.dev/img/EvB4_PHQpq-960.jpeg 960w" sizes="auto"></picture> <figcaption><a href="https://www.oocities.org/siliconvalley/haven/1520/">Lisa's Graphics</a></figcaption> </figure> <p>...and some were fansites. Look at those frames! I got this screenshot mid-<code>&lt;marquee&gt;</code>-scroll as well.</p> <figure> <picture><source type="image/webp" srcset="https://localghost.dev/img/_EqhMEsGpU-280.webp 280w, https://localghost.dev/img/_EqhMEsGpU-640.webp 640w, https://localghost.dev/img/_EqhMEsGpU-960.webp 960w" sizes="auto"><img loading="lazy" decoding="async" src="https://localghost.dev/img/_EqhMEsGpU-280.jpeg" alt="A Final Fantasy fansite" width="960" height="713" srcset="https://localghost.dev/img/_EqhMEsGpU-280.jpeg 280w, https://localghost.dev/img/_EqhMEsGpU-640.jpeg 640w, https://localghost.dev/img/_EqhMEsGpU-960.jpeg 960w" sizes="auto"></picture> <figcaption><a href="https://www.oocities.org/hcdohl/">Mognet Central</a></figcaption> </figure> <p>I played a game a couple of years ago called <a href="https://www.hypnospace.net/">Hypnospace Outlaw</a>, which is a completely bonkers game where you're a moderator of a version of the 90s web that you access in your sleep. The homepages in this game were directly inspired by Geocities websites (there's a <a href="https://noclippodcast.net/episodes/2021/5/22/noclip-pocket-e42-big-winrar-energy-hypnospace-outlaw">really good episode of Noclip</a> about it) and it made me so nostalgic. I really recommend it if you haven't played it already! It really captures the spirit of the time – the personality and weirdness that made these sites so special.</p> <figure> <picture><source type="image/webp" srcset="https://localghost.dev/img/1wR3aT3eJe-280.webp 280w, https://localghost.dev/img/1wR3aT3eJe-640.webp 640w, https://localghost.dev/img/1wR3aT3eJe-960.webp 960w" sizes="auto"><img loading="lazy" decoding="async" src="https://localghost.dev/img/1wR3aT3eJe-280.jpeg" alt="A screenshot from the video game Hypnospace Outlaw, with a website called 'Linda's Library of Weird'." width="960" height="600" srcset="https://localghost.dev/img/1wR3aT3eJe-280.jpeg 280w, https://localghost.dev/img/1wR3aT3eJe-640.jpeg 640w, https://localghost.dev/img/1wR3aT3eJe-960.jpeg 960w" sizes="auto"></picture> <figcaption><a href="https://www.hypnospace.net/">Hypnospace Outlaw</a></figcaption></figure> <h2 id="let-s-bring-back-the-weird" tabindex="-1">Let's bring back the weird</h2> <p>I'd love to see this spirit return today – the experimental and fun side of the web. My goal is to show you how we can be just as creative today but using <strong>modern and accessible methods</strong>. Because, as fun as they were, old websites were a <em>nightmare</em> for accessibility. We didn't really use semantic HTML, we used tables for <em>layouts</em> (instead of, y'know, tabular data), everything was constantly flashing and moving. Luckily for us, the modern web allows us to be just as creative while still considering the user at the other end of the browser.</p> <p>So naturally, I built a <a href="https://sophieswebsite1999.neocities.org/">90s-style website</a>, with some of my favourite old web tropes. I used as much modern HTML, CSS and JS as I could. Let's take a look through some of the features and how we might recreate them!</p> <figure> <picture><source type="image/webp" srcset="https://localghost.dev/img/_j4pPyflU2-280.webp 280w, https://localghost.dev/img/_j4pPyflU2-640.webp 640w, https://localghost.dev/img/_j4pPyflU2-960.webp 960w" sizes="auto"><img loading="lazy" decoding="async" src="https://localghost.dev/img/_j4pPyflU2-280.jpeg" alt="A screenshot of my 90s-style website" width="960" height="625" srcset="https://localghost.dev/img/_j4pPyflU2-280.jpeg 280w, https://localghost.dev/img/_j4pPyflU2-640.jpeg 640w, https://localghost.dev/img/_j4pPyflU2-960.jpeg 960w" sizes="auto"></picture> <figcaption><a href="https://sophieswebsite1999.neocities.org/">Sophie's Homepage</a></figcaption> </figure> <h2 id="animated-gi-fs" tabindex="-1">Animated GIFs</h2> <p>GeoCities sites were absolutely littered with GIFs. Flames, construction workers, dividers, even animated bullet points. Animations were a lot of fun, and almost an art form to squeeze so much into such a tiny filesize.</p> <picture><source type="image/webp" srcset="https://localghost.dev/img/jYNArJwRm1-280.webp 280w, https://localghost.dev/img/jYNArJwRm1-640.webp 640w, https://localghost.dev/img/jYNArJwRm1-960.webp 960w" sizes="auto"><img loading="lazy" decoding="async" src="https://localghost.dev/img/jYNArJwRm1-280.jpeg" alt="A screenshot of cameronsworld.net featuring many space-themed GIFs" width="960" height="625" srcset="https://localghost.dev/img/jYNArJwRm1-280.jpeg 280w, https://localghost.dev/img/jYNArJwRm1-640.jpeg 640w, https://localghost.dev/img/jYNArJwRm1-960.jpeg 960w" sizes="auto"></picture> <p>This is a screenshot from <a href="https://cameronsworld.net">cameronsworld.net</a>, which is a beautiful archive of GeoCities GIFs, and an artwork in itself.</p> <p>Nowadays it's easier than ever to put animations on our sites, whether that's still the humble GIF (internet is so much faster these days), more modern formats like <code>webm</code> and <code>gifv</code>, or even SVG animation with CSS or libraries like GreenSock. But we can do better still.</p> <p>The standard code to include an image hasn't changed much since the olden days:</p> <pre class="language-html"><code class="language-html"><span class="token tag"><span class="token tag"><span class="token punctuation">&lt;</span>IMG</span> <span class="token attr-name">SRC</span><span class="token attr-value"><span class="token punctuation attr-equals">=</span><span class="token punctuation">"</span>flames.gif<span class="token punctuation">"</span></span><span class="token punctuation">></span></span></code></pre> <picture eleventy:ignore=""> <source srcset="https://localghost.dev/img/geocities/flames.gif" media="(prefers-reduced-motion: no-preference)"> <img eleventy:ignore="" src="https://localghost.dev/img/geocities/static/flames.png" alt="Animated flames"> </picture> <p>In those days, of course, we wrote all our HTML in capitals because that was what you did for some reason. XHTML, maybe? Anyway, the consequence of this is that everyone sees the GIF whether they like it or not. For people with epilepsy, vestibular disorders, or anything where motion causes sickness, autoplaying GIFs are a big problem. Luckily, wcan fix this today, with something we didn't have back then!</p> <h3 id="the-prefers-reduced-motion-media-query" tabindex="-1">The <code>prefers-reduced-motion</code> media query</h3> <p>Using this media query, we can only play the GIF if the user doesn't have reduced motion turned on on their computer – so everyone can enjoy our trash website, regardless of their access needs.</p> <h3 id="harnessing-media-queries-with-the-picture-element" tabindex="-1">Harnessing media queries with the <code>picture</code> element</h3> <p>The HTML5 <code>picture</code> element allows us to specify an image, and then potential alternative sources for it.</p> <pre class="language-html"><code class="language-html"> <span class="token tag"><span class="token tag"><span class="token punctuation">&lt;</span>picture</span><span class="token punctuation">></span></span> <span class="token tag"><span class="token tag"><span class="token punctuation">&lt;</span>source</span> <span class="token attr-name">srcset</span><span class="token attr-value"><span class="token punctuation attr-equals">=</span><span class="token punctuation">"</span>underconstruction.gif<span class="token punctuation">"</span></span> <span class="token attr-name">media</span><span class="token attr-value"><span class="token punctuation attr-equals">=</span><span class="token punctuation">"</span>(prefers-reduced-motion: no-preference)<span class="token punctuation">"</span></span><span class="token punctuation">></span></span> <span class="token tag"><span class="token tag"><span class="token punctuation">&lt;</span>img</span> <span class="token attr-name">src</span><span class="token attr-value"><span class="token punctuation attr-equals">=</span><span class="token punctuation">"</span>underconstruction.png<span class="token punctuation">"</span></span> <span class="token attr-name">alt</span><span class="token attr-value"><span class="token punctuation attr-equals">=</span><span class="token punctuation">"</span>Under construction<span class="token punctuation">"</span></span> <span class="token punctuation">/></span></span> <span class="token tag"><span class="token tag"><span class="token punctuation">&lt;/</span>picture</span><span class="token punctuation">></span></span></code></pre> <p>In the above code snippet, we've got the <code>img</code> tag as before, but this time it's showing a <em>still</em> version of the GIF, which I made by opening the GIF in Preview, pulling out the first frame and saving it as a PNG. The <code>source</code> tag contains the URL of the GIF, and will only kick in if its <code>media</code> attribute is satisfied. So if you don't have reduced motion enabled, the source of the image will be replaced by the animated GIF version. Magic!</p> <picture eleventy:ignore=""> <source srcset="https://localghost.dev/img/geocities/consbar.gif" media="(prefers-reduced-motion: no-preference)"> <img eleventy:ignore="" src="https://localghost.dev/img/geocities/static/consbar.png" alt="Under construction"> </picture> <h2 id="text-effects" tabindex="-1">Text effects</h2> <p>Remember <code>&lt;marquee&gt;</code>? It made text scroll across the screen, like ticker tape.</p> <p><span class="marquee-wrapper"><span class="marquee">Wheee!</span></span></p> <p>Or those of you with Netscape would have had the infamous <code>&lt;blink&gt;</code> tag, which makes text blink in and out of view...</p> <p><span class="blink">This is awful</span></p> <p>Ultimately, text effects like this don't belong in body text. Even if you don't have any access needs to speak of, it makes reading text really hard. There's a good reason they were both deprecated.</p> <p>Instead, I thought, why not have fun with headers? In days of yore we'd make cool text-based headers in whatever graphics programs we could get our hands on – or even just MS Word. Text with flames, rainbow fonts, you name it.</p> <p>These days we can recreate this magic using CSS instead of using an image! And the great news is, because it's normal text with CSS doing the heavy lifting, it's still totally accessible.</p> <p>For my next trick, I'm drawing inspiration from an OG 90s classic: Microsoft WordArt.</p> <picture><source type="image/webp" srcset="https://localghost.dev/img/V0cmDrZS7n-280.webp 280w, https://localghost.dev/img/V0cmDrZS7n-409.webp 409w" sizes="auto"><img loading="lazy" decoding="async" src="https://localghost.dev/img/V0cmDrZS7n-280.jpeg" alt="MS WordArt selection gallery" width="409" height="336" srcset="https://localghost.dev/img/V0cmDrZS7n-280.jpeg 280w, https://localghost.dev/img/V0cmDrZS7n-409.jpeg 409w" sizes="auto"></picture> <h3 id="word-art-but-make-it-css" tabindex="-1">WordArt, but make it CSS</h3> <p>While not strictly from the 90s web, WordArt for me harks back to the aesthetic of 90s maximalism, and definitely fits aesthetically with what I'm trying to do.</p> <p>I'm going to show you how to recreate two of my classic favourite WordArt styles using modern CSS.</p> <picture><source type="image/webp" srcset="https://localghost.dev/img/x1WaTgX3qR-280.webp 280w, https://localghost.dev/img/x1WaTgX3qR-640.webp 640w, https://localghost.dev/img/x1WaTgX3qR-960.webp 960w" sizes="auto"><img loading="lazy" decoding="async" src="https://localghost.dev/img/x1WaTgX3qR-280.jpeg" alt="The word 'wordart' in Impact font, skewed to point up and to the right, filled in with a purple gradient and a light purple drop shadow" width="960" height="506" srcset="https://localghost.dev/img/x1WaTgX3qR-280.jpeg 280w, https://localghost.dev/img/x1WaTgX3qR-640.jpeg 640w, https://localghost.dev/img/x1WaTgX3qR-960.jpeg 960w" sizes="auto"></picture> <h4 id="gradient-fill-text-with-background-clip" tabindex="-1">Gradient-fill text with <code>background-clip</code></h4> <p>You can't colour text with a gradient (yet) in CSS, but you <em>can</em> give an element a gradient fill <em>background</em>. Using the <a href="https://developer.mozilla.org/en-US/docs/Web/CSS/background-clip"><code>background-clip</code></a> property we can control where the background shows. Specifically, we can set <code>background-clip: text</code> to make the background only show wherever there's text in the element.</p> <p>Then, if we make the actual text transparent, only the gradient background will show through.</p> <pre class="language-css"><code class="language-css"><span class="token property">background</span><span class="token punctuation">:</span> <span class="token function">linear-gradient</span><span class="token punctuation">(</span>183deg<span class="token punctuation">,</span> #6000CA 10%<span class="token punctuation">,</span> #CA00CD 70%<span class="token punctuation">)</span><span class="token punctuation">;</span> <span class="token property">background-clip</span><span class="token punctuation">:</span> text<span class="token punctuation">;</span> <span class="token property">-webkit-background-clip</span><span class="token punctuation">:</span> text<span class="token punctuation">;</span> <span class="token property">color</span><span class="token punctuation">:</span> transparent<span class="token punctuation">;</span> <span class="token property">font-family</span><span class="token punctuation">:</span> <span class="token string">'Impact'</span><span class="token punctuation">;</span></code></pre> <p class="purple-wordart-base">WordArt</p> <p>Pretty!</p> <p>Then let's add a <code>transform</code> property to make it look a bit more like the real deal.</p> <pre class="language-css"><code class="language-css"><span class="token property">transform</span><span class="token punctuation">:</span> <span class="token function">skewY</span><span class="token punctuation">(</span>-8deg<span class="token punctuation">)</span> <span class="token function">scaleY</span><span class="token punctuation">(</span>1.3<span class="token punctuation">)</span> <span class="token function">scaleX</span><span class="token punctuation">(</span>0.8<span class="token punctuation">)</span><span class="token punctuation">;</span></code></pre> <p class="purple-wordart-base purple-wordart-skewed">WordArt</p> <h4 id="adding-the-drop-shadow" tabindex="-1">Adding the drop shadow</h4> <p>Now we need to add the light purple drop shadow. But if we try to use the <code>text-shadow</code> property, it shows up on top of the text!</p> <p class="purple-wordart-base purple-wordart-skewed purple-wordart-text-shadow">WordArt</p> <p>This is because we're really looking at the background – the actual text is transparent and sitting on top. If I change the colour of the text to the body colour, you'll see what I mean:</p> <p class="purple-wordart-base purple-wordart-skewed purple-wordart-text-shadow purple-wordart-text-black">WordArt</p> <p>To get around this, we'll need a wrapper element that contains the shadow, so it appears behind the text-shaped gradient background.</p> <pre class="language-html"><code class="language-html"><span class="token tag"><span class="token tag"><span class="token punctuation">&lt;</span>span</span> <span class="token attr-name">class</span><span class="token attr-value"><span class="token punctuation attr-equals">=</span><span class="token punctuation">"</span>purple-wordart-wrapper<span class="token punctuation">"</span></span><span class="token punctuation">></span></span> <span class="token tag"><span class="token tag"><span class="token punctuation">&lt;</span>span</span> <span class="token attr-name">class</span><span class="token attr-value"><span class="token punctuation attr-equals">=</span><span class="token punctuation">"</span>purple-wordart<span class="token punctuation">"</span></span><span class="token punctuation">></span></span>WordArt<span class="token tag"><span class="token tag"><span class="token punctuation">&lt;/</span>span</span><span class="token punctuation">></span></span> <span class="token tag"><span class="token tag"><span class="token punctuation">&lt;/</span>span</span><span class="token punctuation">></span></span></code></pre> <p>We'll add a <strong>drop-shadow filter</strong> to the wrapper element. This adds a drop-shadow the same shape as the element's children, and because the background is clipped to the text in the child <code>span</code>, the drop-shadow will follow that shape too!</p> <pre class="language-css"><code class="language-css"><span class="token selector">.wordart-wrapper</span> <span class="token punctuation">{</span> <span class="token property">filter</span><span class="token punctuation">:</span> <span class="token function">drop-shadow</span><span class="token punctuation">(</span>2px 2px 0px <span class="token function">rgba</span><span class="token punctuation">(</span>130<span class="token punctuation">,</span> 140<span class="token punctuation">,</span> 251<span class="token punctuation">,</span> 0.8<span class="token punctuation">)</span><span class="token punctuation">)</span><span class="token punctuation">;</span> <span class="token punctuation">}</span></code></pre> <p>The result:</p> <span class="purple-wordart-wrapper"> <span class="purple-wordart-base purple-wordart-skewed">WordArt</span></span> <p>Uncanny!</p> <p>For my second WordArt recreation, I'm bringing back my old childhood favourite – the rainbow one. I remember using this one all over my primary school homework.</p> <picture><source type="image/webp" srcset="https://localghost.dev/img/L2U9CwS5X--280.webp 280w, https://localghost.dev/img/L2U9CwS5X--640.webp 640w, https://localghost.dev/img/L2U9CwS5X--960.webp 960w" sizes="auto"><img loading="lazy" decoding="async" src="https://localghost.dev/img/L2U9CwS5X--280.jpeg" alt="MS Wordart screenshot, the word 'WordArt' written in rainbow gradient text with a grey 3D shadow." width="960" height="443" srcset="https://localghost.dev/img/L2U9CwS5X--280.jpeg 280w, https://localghost.dev/img/L2U9CwS5X--640.jpeg 640w, https://localghost.dev/img/L2U9CwS5X--960.jpeg 960w" sizes="auto"></picture> <p>We've got another gradient fill here, so we'll use <code>background-clip: text</code> again to get the same effect, and chuck on a <code>transform</code> to get the right shape.</p> <pre class="language-css"><code class="language-css"><span class="token property">background</span><span class="token punctuation">:</span> <span class="token function">linear-gradient</span><span class="token punctuation">(</span> 90deg<span class="token punctuation">,</span> #9c00ff<span class="token punctuation">,</span> #ff0000<span class="token punctuation">,</span> #ff8800<span class="token punctuation">,</span> #ffff00<span class="token punctuation">,</span> #02be02<span class="token punctuation">,</span> #0000ff<span class="token punctuation">,</span> #4f00ff<span class="token punctuation">,</span> #9c00ff <span class="token punctuation">)</span><span class="token punctuation">;</span> <span class="token property">background-clip</span><span class="token punctuation">:</span> text<span class="token punctuation">;</span> <span class="token property">-webkit-background-clip</span><span class="token punctuation">:</span> text<span class="token punctuation">;</span> <span class="token property">color</span><span class="token punctuation">:</span> transparent<span class="token punctuation">;</span> <span class="token property">font-family</span><span class="token punctuation">:</span> <span class="token string">'Arial Black'</span><span class="token punctuation">,</span> sans-serif<span class="token punctuation">;</span> <span class="token property">font-weight</span><span class="token punctuation">:</span> bold<span class="token punctuation">;</span> <span class="token property">transform</span><span class="token punctuation">:</span> <span class="token function">scaleY</span><span class="token punctuation">(</span>1.5<span class="token punctuation">)</span> <span class="token function">scaleX</span><span class="token punctuation">(</span>0.6<span class="token punctuation">)</span><span class="token punctuation">;</span> <span class="token property">transform-origin</span><span class="token punctuation">:</span> left<span class="token punctuation">;</span></code></pre> <p><span class="rainbow constrain-width">WordArt</span></p> <p>We'll use a wrapper element to create the shadow again, but this time it's a little different. This has more of a 3D effect, where the shadow kind of flattens and goes to the left, as if we're looking at the WordArt from the front.</p> <p>CSS can do that!</p> <h4 id="getting-some-perspective" tabindex="-1">Getting some perspective</h4> <pre class="language-css"><code class="language-css"><span class="token selector">.wrapper</span> <span class="token punctuation">{</span> <span class="token property">font-family</span><span class="token punctuation">:</span> <span class="token string">'Arial Black'</span><span class="token punctuation">,</span> sans-serif<span class="token punctuation">;</span> <span class="token property">font-weight</span><span class="token punctuation">:</span> bold<span class="token punctuation">;</span> <span class="token property">display</span><span class="token punctuation">:</span> inline-block<span class="token punctuation">;</span> <span class="token property">position</span><span class="token punctuation">:</span> relative<span class="token punctuation">;</span> <span class="token property">perspective</span><span class="token punctuation">:</span> 150px<span class="token punctuation">;</span> <span class="token property">perspective-origin</span><span class="token punctuation">:</span> bottom center<span class="token punctuation">;</span> <span class="token punctuation">}</span></code></pre> <p>We can use the <code>perspective</code> property to put us into a kind of &quot;3D mode&quot;. It tells the browser, &quot;act as though I'm standing this far away from the element&quot;. In our case, 150px.</p> <p>We then set the <code>perspective-origin</code> property to determine what position we're looking at the element from. I want it to seem like we're in front of it, at the bottom.</p> <p>What this will do is change the way that transformations apply to the element, taking into account the perspective to manipulate it along the Z-axis as well as X and Y.</p> <p>To create a shadow effect I'll target the <code>wrapper::before</code> pseudoelement, and set its content to &quot;WordArt&quot; to mirror the text. This will make the &quot;shadow&quot; text appear behind the rainbow gradient. Then I'll apply some transformations to skew the &quot;shadow&quot; – that <code>perspective</code> property on the <code>wrapper</code> element will change the way it rotates and skews.</p> <p>(This one needs a bit more hacky wrangling when you change the font size, and I use fluid typescales so I'm going to embed a Codepen a bit further down instead of rendering it inline!)</p> <pre class="language-css"><code class="language-css"><span class="token selector">.wrapper::before</span> <span class="token punctuation">{</span> <span class="token property">position</span><span class="token punctuation">:</span> absolute<span class="token punctuation">;</span> <span class="token property">content</span><span class="token punctuation">:</span> <span class="token string">'WordArt'</span><span class="token punctuation">;</span> <span class="token property">color</span><span class="token punctuation">:</span> #000<span class="token punctuation">;</span> <span class="token property">opacity</span><span class="token punctuation">:</span> 0.2<span class="token punctuation">;</span> <span class="token property">bottom</span><span class="token punctuation">:</span> -2rem<span class="token punctuation">;</span> <span class="token property">left</span><span class="token punctuation">:</span>35%<span class="token punctuation">;</span> <span class="token property">transform</span><span class="token punctuation">:</span> <span class="token function">rotateX</span><span class="token punctuation">(</span>60deg<span class="token punctuation">)</span> <span class="token function">skewX</span><span class="token punctuation">(</span>65deg<span class="token punctuation">)</span> <span class="token function">scaleY</span><span class="token punctuation">(</span>2.8<span class="token punctuation">)</span> <span class="token function">scaleX</span><span class="token punctuation">(</span>0.9<span class="token punctuation">)</span><span class="token punctuation">;</span> <span class="token property">transform-origin</span><span class="token punctuation">:</span> bottom right<span class="token punctuation">;</span> <span class="token punctuation">}</span></code></pre> <p>Right now, the CSS is hardcoded to have a shadow that says &quot;WordArt&quot;. If we want to use this text style for other things too, how can we dynamically set the shadow text content? With the CSS <code>attr()</code> function!</p> <p><code>attr()</code> gets the content of a given attribute for an element. I've called mine <code>data-content</code>. So, in our <code>wrapper::before</code> rule, <code>content: 'WordArt'</code> becomes <code>content: attr(data-content)</code>:</p> <pre class="language-css"><code class="language-css"><span class="token selector">.wrapper::before</span> <span class="token punctuation">{</span> <span class="token property">position</span><span class="token punctuation">:</span> absolute<span class="token punctuation">;</span> <span class="token property">content</span><span class="token punctuation">:</span> <span class="token function">attr</span><span class="token punctuation">(</span>data-content<span class="token punctuation">)</span><span class="token punctuation">;</span> <span class="token property">color</span><span class="token punctuation">:</span> #000<span class="token punctuation">;</span> <span class="token property">opacity</span><span class="token punctuation">:</span> 0.2<span class="token punctuation">;</span> <span class="token property">bottom</span><span class="token punctuation">:</span> -1rem<span class="token punctuation">;</span> <span class="token property">transform</span><span class="token punctuation">:</span> <span class="token function">rotateX</span><span class="token punctuation">(</span>60deg<span class="token punctuation">)</span> <span class="token function">skewX</span><span class="token punctuation">(</span>60deg<span class="token punctuation">)</span> <span class="token function">scaleY</span><span class="token punctuation">(</span>2.8<span class="token punctuation">)</span> <span class="token function">scaleX</span><span class="token punctuation">(</span>0.8<span class="token punctuation">)</span><span class="token punctuation">;</span> <span class="token property">transform-origin</span><span class="token punctuation">:</span> bottom right<span class="token punctuation">;</span> <span class="token punctuation">}</span></code></pre> <p>And render the HTML with the attribute:</p> <pre class="language-html"><code class="language-html"><span class="token tag"><span class="token tag"><span class="token punctuation">&lt;</span>span</span> <span class="token attr-name">class</span><span class="token attr-value"><span class="token punctuation attr-equals">=</span><span class="token punctuation">"</span>rainbow-wrapper<span class="token punctuation">"</span></span> <span class="token attr-name">data-content</span><span class="token attr-value"><span class="token punctuation attr-equals">=</span><span class="token punctuation">"</span>Rainbow<span class="token punctuation">"</span></span><span class="token punctuation">></span></span> <span class="token tag"><span class="token tag"><span class="token punctuation">&lt;</span>span</span> <span class="token attr-name">class</span><span class="token attr-value"><span class="token punctuation attr-equals">=</span><span class="token punctuation">"</span>rainbow<span class="token punctuation">"</span></span><span class="token punctuation">></span></span>Rainbow<span class="token tag"><span class="token tag"><span class="token punctuation">&lt;/</span>span</span><span class="token punctuation">></span></span> <span class="token tag"><span class="token tag"><span class="token punctuation">&lt;/</span>span</span><span class="token punctuation">></span></span></code></pre> <p>Now we can write different words!</p> <p class="codepen" data-height="265" data-theme-id="dark" data-default-tab="result" data-user="sophiekoonin" data-slug-hash="qBKmzmR" data-preview="true" data-pen-title="Rainbow text"> <span>See the Pen <a href="https://codepen.io/sophiekoonin/pen/qBKmzmR"> Rainbow text</a> by <a href="https://codepen.io/sophiekoonin">@sophiekoonin</a> on <a href="https://codepen.io">CodePen</a>.</span> </p> <script async="" src="https://cpwebassets.codepen.io/assets/embed/ei.js"></script> <p>It's not perfect, but I think that looks good enough for a throwback site header!</p> <h2 id="music" tabindex="-1">Music</h2> <p>In 2001, I had a NeoPets shop. As soon as you loaded the page, you'd be greeted with the dulcet tones of Teenage Dirtbag, in MIDI form.</p> <pre class="language-html"><code class="language-html"><span class="token tag"><span class="token tag"><span class="token punctuation">&lt;</span>EMBED</span> <span class="token attr-name">SRC</span><span class="token attr-value"><span class="token punctuation attr-equals">=</span><span class="token punctuation">"</span>mysong.mid<span class="token punctuation">"</span></span> <span class="token attr-name">HIDDEN</span><span class="token attr-value"><span class="token punctuation attr-equals">=</span><span class="token punctuation">"</span>true<span class="token punctuation">"</span></span> <span class="token attr-name">loop</span><span class="token attr-value"><span class="token punctuation attr-equals">=</span><span class="token punctuation">"</span>yes<span class="token punctuation">"</span></span> <span class="token attr-name">volume</span><span class="token attr-value"><span class="token punctuation attr-equals">=</span><span class="token punctuation">"</span>10<span class="token punctuation">"</span></span> <span class="token attr-name">autostart</span><span class="token attr-value"><span class="token punctuation attr-equals">=</span><span class="token punctuation">"</span>true<span class="token punctuation">"</span></span><span class="token punctuation">></span></span></code></pre> <p>Autoplay, naturally, and looping. Definitely hidden, so the music was just <em>there</em>. But it was super distracting, quite disorientating, and you couldn't turn it off.</p> <p>Modern browsers block autoplaying audio, for good reason. It's extremely annoying. Thankfully, the HTML5 <code>audio</code> element gives us a bit more control.</p> <pre class="language-html"><code class="language-html"> <span class="token tag"><span class="token tag"><span class="token punctuation">&lt;</span>audio</span> <span class="token attr-name">aria-label</span><span class="token attr-value"><span class="token punctuation attr-equals">=</span><span class="token punctuation">"</span>Play music<span class="token punctuation">"</span></span> <span class="token attr-name">controls</span> <span class="token attr-name">src</span><span class="token attr-value"><span class="token punctuation attr-equals">=</span><span class="token punctuation">"</span>/soundtrack.webm<span class="token punctuation">"</span></span><span class="token punctuation">></span></span><span class="token tag"><span class="token tag"><span class="token punctuation">&lt;/</span>audio</span><span class="token punctuation">></span></span></code></pre> <p>You can still have audio on your website! Just make it <em>opt-in</em>. If it's part of the experience you want to create, that's totally fine, as long as your viewer is okay with it playing.</p> <p><audio aria-label="Play music" controls="" src="https://localghost.dev/extras/soundtrack.webm"></audio></p> <p>Make sure to add a label – whether external or <code>aria-label</code> – to tell the user what it does.</p> <p>The controls that show up are the browser default – what's rendered above will look different depending on whether you're in Chrome, Firefox, etc. But you can use the <a href="https://developer.mozilla.org/en-US/docs/Web/API/Web_Audio_API/Using_Web_Audio_API#controlling_sound">Web Audio API</a> to customise the controls, by rendering some pretty buttons and using JavaScript to make them control playing audio.</p> <h2 id="cursor-trails" tabindex="-1">Cursor trails</h2> <h3 id="then-dynamic-html" tabindex="-1">Then: Dynamic HTML</h3> <p>Back in the day, a cursor trail was a real flex. It said, &quot;look at what I can do with JavaScript!&quot;. (Or in my case, what <a href="https://dynamicdrive.com">dynamicdrive.com</a> could do with JavaScript.)</p> <p>This was known as Dynamic HTML: not necessarily a technology in its own right, but a collection of technologies (a bit like we use the term JAMstack now). HTML, CSS and JavaScript – but a very old version of JavaScript. Everything was client-side at this time, because we didn't have AJAX/client HTTP requests yet. And the implementations differed significantly between the two major browsers of the time, Internet Explorer and Netscape. (This is a period known as the 'Browser Wars', and there's a <a href="https://thehistoryoftheweb.com/browser-wars/">good summary of it on The History Of The Web</a>.)</p> <p>It led to stuff like this, from a <a href="https://www.dynamicdrive.com/dynamicindex13/star.htm">Dancing Stars animated cursor trail</a>. This cursor adds a trail of seven 3px-wide yellow &quot;stars&quot; (tiny squares) that appear to follow your cursor around. I can't show you a preview, because the script doesn't work any more.</p> <p>First we had to check whether the script was running in IE, or Netscape, because the implementation would be completely different. In IE, we'd check for the existence of <code>document.all</code>, a function which returned all the elements in the DOM.</p> <p>If this was present, we'd then call <code>document.write</code> (a function we also <a href="https://developer.mozilla.org/en-US/docs/Web/API/Document/write">shouldn't use any more</a>) to insert several little 3px <code>&lt;div&gt;</code> squares into the DOM.</p> <pre class="language-js"><code class="language-js"><span class="token keyword">if</span> <span class="token punctuation">(</span>document<span class="token punctuation">.</span>all<span class="token punctuation">)</span> <span class="token punctuation">{</span> document<span class="token punctuation">.</span><span class="token function">write</span><span class="token punctuation">(</span>'<span class="token operator">&lt;</span>div id<span class="token operator">=</span><span class="token string">"starsDiv"</span> style<span class="token operator">=</span><span class="token string">"position:absolute;top:0px;left:0px"</span><span class="token operator">></span>'<span class="token punctuation">)</span> <span class="token keyword">for</span> <span class="token punctuation">(</span>xy <span class="token operator">=</span> <span class="token number">0</span><span class="token punctuation">;</span> xy <span class="token operator">&lt;</span> <span class="token number">7</span><span class="token punctuation">;</span> xy<span class="token operator">++</span><span class="token punctuation">)</span> document<span class="token punctuation">.</span><span class="token function">write</span><span class="token punctuation">(</span>'<span class="token operator">&lt;</span>div style<span class="token operator">=</span>"position<span class="token operator">:</span>relative<span class="token punctuation">;</span> <span class="token literal-property property">width</span><span class="token operator">:</span>3px<span class="token punctuation">;</span>height<span class="token operator">:</span>3px<span class="token punctuation">;</span>background<span class="token operator">:</span>#<span class="token constant">FFFF00</span><span class="token punctuation">;</span> font<span class="token operator">-</span>size<span class="token operator">:</span>2px<span class="token punctuation">;</span>visibility<span class="token operator">:</span>visible"<span class="token operator">></span><span class="token operator">&lt;</span><span class="token operator">/</span>div<span class="token operator">></span>'<span class="token punctuation">)</span> document<span class="token punctuation">.</span><span class="token function">write</span><span class="token punctuation">(</span><span class="token string">'&lt;/div>'</span><span class="token punctuation">)</span> <span class="token punctuation">}</span></code></pre> <p>Netscape didn't have <code>div</code>s, it had <code>LAYER</code>s. This is basically the same thing with a different name, because browser creators at the time liked causing pain.</p> <p>(Side note, this is why some people used to refer to <code>divs</code> as &quot;div layers&quot; – it encompasses both elements.)</p> <pre class="language-html"><code class="language-html"><span class="token tag"><span class="token tag"><span class="token punctuation">&lt;</span>LAYER</span> <span class="token attr-name">NAME</span><span class="token attr-value"><span class="token punctuation attr-equals">=</span><span class="token punctuation">"</span>a0<span class="token punctuation">"</span></span> <span class="token attr-name">LEFT</span><span class="token attr-value"><span class="token punctuation attr-equals">=</span>10</span> <span class="token attr-name">TOP</span><span class="token attr-value"><span class="token punctuation attr-equals">=</span>10</span> <span class="token attr-name">VISIBILITY</span><span class="token attr-value"><span class="token punctuation attr-equals">=</span>SHOW</span> <span class="token attr-name">BGCOLOR</span><span class="token attr-value"><span class="token punctuation attr-equals">=</span><span class="token punctuation">"</span>#FFFF00<span class="token punctuation">"</span></span> <span class="token attr-name">CLIP</span><span class="token attr-value"><span class="token punctuation attr-equals">=</span><span class="token punctuation">"</span>0,0,3,3<span class="token punctuation">"</span></span><span class="token punctuation">></span></span> <span class="token tag"><span class="token tag"><span class="token punctuation">&lt;/</span>LAYER</span><span class="token punctuation">></span></span> }</code></pre> <p>If <code>document.layers</code> returned something, we knew we were in Netscape, and could do Netscape things. In this case:</p> <pre class="language-js"><code class="language-js"><span class="token keyword">if</span> <span class="token punctuation">(</span>document<span class="token punctuation">.</span>layers<span class="token punctuation">)</span> <span class="token punctuation">{</span> window<span class="token punctuation">.</span><span class="token function">captureEvents</span><span class="token punctuation">(</span>Event<span class="token punctuation">.</span><span class="token constant">MOUSEMOVE</span><span class="token punctuation">)</span><span class="token punctuation">;</span> <span class="token punctuation">}</span></code></pre> <p>The full script is available at <a href="http://www.dynamicdrive.com/dynamicindex13/star.htm">DynamicDrive.com</a>.</p> <h3 id="now-canvas-and-request-animation-frame" tabindex="-1">Now: Canvas and <code>requestAnimationFrame</code></h3> <p><a href="https://tholman.com/cursor-effects">Tim Holman</a> has done the hard work for us here, and recreated 90s-style cursor effects with modern JavaScript and HTML.</p> <video controls=""> <source src="https://localghost.dev/img/blog/build-1999/star-cursor.webm" type="video/webm"> Video of a cursor trail with many small coloured stars following the cursor. <a href="https://localghost.dev/img/blog/build-1999/star-cursor.webm">Download the video as .webm</a> </video> <p>Tim's cursors use canvas, which is much more performant than rendering individual elements into the DOM. You also get much more fine-grained control over how elements are positioned. Can you imagine trying to render this many stars into the DOM in the old script? Using the <a href="https://developer.mozilla.org/en-US/docs/Web/API/Canvas_API">canvas API</a> and <a href="https://developer.mozilla.org/en-US/docs/Web/API/window/requestAnimationFrame"><code>requestAnimationFrame</code></a> – the function that allows us to batch up animations and efficiently update them before the browser's next repaint – we can render lots of little stars, making them fade in and out in lovely ways.</p> <h3 id="media-queries-work-in-java-script-too" tabindex="-1">Media queries work in JavaScript, too!</h3> <p>Of course, cursor trails mean animations, and not everyone wants to see those. The good news is, we can use media queries in JS, just like we do in CSS and HTML <code>&lt;source&gt;</code> tags.</p> <pre class="language-js"><code class="language-js"><span class="token keyword">const</span> prefersReducedMotionQuery <span class="token operator">=</span> window<span class="token punctuation">.</span><span class="token function">matchMedia</span><span class="token punctuation">(</span><span class="token string">'prefers-reduced-motion'</span><span class="token punctuation">)</span> <span class="token keyword">if</span> <span class="token punctuation">(</span><span class="token operator">!</span>prefersReducedMotionQuery<span class="token punctuation">.</span>matches<span class="token punctuation">)</span> <span class="token punctuation">{</span> cursor<span class="token punctuation">.</span><span class="token function">init</span><span class="token punctuation">(</span><span class="token punctuation">)</span> <span class="token punctuation">}</span></code></pre> <p>By calling <code>window.matchMedia</code> with the name of the query we're interested in (<code>prefers-reduced-motion</code>), we can check the value of the user's motion settings and only init the cursor if they <em>don't</em> have reduced motion enabled.</p> <p>You can even add an event listener to the media query, so that when its value changes we can dynamically initialise or destroy the cursor accordingly.</p> <pre class="language-js"><code class="language-js">prefersReducedMotionQuery<span class="token punctuation">.</span><span class="token function">addEventListener</span><span class="token punctuation">(</span><span class="token string">'change'</span><span class="token punctuation">,</span> <span class="token punctuation">(</span><span class="token punctuation">)</span> <span class="token operator">=></span> <span class="token punctuation">{</span> <span class="token keyword">if</span> <span class="token punctuation">(</span>prefersReducedMotionQuery<span class="token punctuation">.</span>matches<span class="token punctuation">)</span> <span class="token punctuation">{</span> cursor<span class="token punctuation">.</span><span class="token function">destroy</span><span class="token punctuation">(</span><span class="token punctuation">)</span> <span class="token punctuation">}</span> <span class="token keyword">else</span> <span class="token punctuation">{</span> cursor<span class="token punctuation">.</span><span class="token function">init</span><span class="token punctuation">(</span><span class="token punctuation">)</span> <span class="token punctuation">}</span> <span class="token punctuation">}</span><span class="token punctuation">)</span> </code></pre> <h2 id="webrings" tabindex="-1">Webrings</h2> <p>In the days before search engines were particularly good, how did you find similar websites? Webrings, of course. A webring is a collection of websites based around a shared interest or topic. Webrings offered a sense of belonging to a community, and gave you a fancy plaque to put on your website.</p> <picture><source type="image/webp" srcset="https://localghost.dev/img/JYSEhcy7C2-280.webp 280w, https://localghost.dev/img/JYSEhcy7C2-640.webp 640w" sizes="auto"><img loading="lazy" decoding="async" src="https://localghost.dev/img/JYSEhcy7C2-280.jpeg" alt="A webring plaque for Calvin and Hobbes-themed sites, with links to previous, next and random sites in the ring, and links to the index page." width="640" height="251" srcset="https://localghost.dev/img/JYSEhcy7C2-280.jpeg 280w, https://localghost.dev/img/JYSEhcy7C2-640.jpeg 640w" sizes="auto"></picture> <p>Each site in the webring would have a plaque like this so you could navigate the ring, and there'd be a backend somewhere (probably written in Perl) that did all the hard work of calculating which site came where in the ring.</p> <p>I built my own <a href="https://sotb22-webring.neocities.org">webring for State of the Browser 2022 attendees</a>, with a Google Sheets backend (for time-saving/live demo reasons) and a Cloudflare Worker on top of that to work out which site to send people to. It checks the value of <code>request.referrer</code> to see where the call is coming from, looks up that URL in the list of sites, and returns the next or previous one accordingly.</p> <pre class="language-js"><code class="language-js"><span class="token keyword">async</span> <span class="token keyword">function</span> <span class="token function">handleRequest</span><span class="token punctuation">(</span><span class="token parameter">request</span><span class="token punctuation">)</span> <span class="token punctuation">{</span> <span class="token keyword">const</span> <span class="token punctuation">{</span> pathname <span class="token punctuation">}</span> <span class="token operator">=</span> <span class="token keyword">new</span> <span class="token class-name">URL</span><span class="token punctuation">(</span>request<span class="token punctuation">.</span>url<span class="token punctuation">)</span> <span class="token keyword">const</span> data <span class="token operator">=</span> <span class="token keyword">await</span> <span class="token function">fetchAndParseCsv</span><span class="token punctuation">(</span><span class="token punctuation">)</span> <span class="token keyword">const</span> referrer <span class="token operator">=</span> request<span class="token punctuation">.</span>referrer <span class="token punctuation">[</span><span class="token operator">...</span><span class="token punctuation">]</span></code></pre> <p>For something a little more permanent/professional, I recommend checking out <a href="https://mxb.dev/blog/webring-kit/">Max Böck's webring kit</a>.</p> <h2 id="guestbooks" tabindex="-1">Guestbooks</h2> <p>Last but not least, the humble guestbook! In the days before social media, this was how we showed our appreciation for webmasters. Rather than just building a guestbook I thought I'd do something a little different for the talk. It's a guestbook all right, but it's powered by Twitter!</p> <figure> <picture><source type="image/webp" srcset="https://localghost.dev/img/YzaA2DliHA-280.webp 280w, https://localghost.dev/img/YzaA2DliHA-640.webp 640w, https://localghost.dev/img/YzaA2DliHA-960.webp 960w" sizes="auto"><img loading="lazy" decoding="async" src="https://localghost.dev/img/YzaA2DliHA-280.jpeg" alt="A screenshot of my guestbook, with messages from conference attendees." width="960" height="625" srcset="https://localghost.dev/img/YzaA2DliHA-280.jpeg 280w, https://localghost.dev/img/YzaA2DliHA-640.jpeg 640w, https://localghost.dev/img/YzaA2DliHA-960.jpeg 960w" sizes="auto"></picture> <figcaption><a href="https://sophieswebsite1999.neocities.org/guestbook">View the guestbook</a></figcaption> </figure> <p>Instead, I used a technology called <strong>webmentions</strong>: a protocol to notify a website when someone else links to them, such as on their own website or on Twitter. Webmentions are collected as a feed (a bit like RSS) and associated with a domain name or host. I put meta tags in the <code>head</code> of my site to indicate that I'm on the lookout for webmentions.</p> <p>I use <a href="https://webmention.io">webmention.io</a> to collect those webmentions for me, though it's totally possible to set up your own server to do so. On this website (localghost) I collect webmentions at build time and publish them underneath the pages, but on the demo website for this talk I have a client-side script to fetch mentions as I wanted to be able to demo them live.</p> <p>I use <a href="https://brid.gy">brid.gy</a> to collect mentions from Twitter and send them to <a href="http://webmention.io">webmention.io</a>, and then my site queries <a href="http://webmention.io">webmention.io</a> to get the feed of mentions.</p> <h2 id="go-forth-and-build-weird-stuff" tabindex="-1">Go forth and build weird stuff!</h2> <p>The web is an amazing platform brimming with opportunities to be creative and experimental. I'd love to see what you build – if you mention this page on your own site or Twitter, the webmentions will appear below, or tag me <code>@type__error</code>!</p> Everything I googled in a week as a senior software engineer 2022-10-15T00:00:00Z https://localghost.dev/blog/everything-i-googled-in-a-week-as-a-senior-software-engineer/ <p>Three years ago I wrote a post called <a href="https://localghost.dev/blog/everything-i-googled-in-a-week-as-a-professional-software-engineer/">Everything I googled in a week as a professional software engineer</a>, and it clearly resonated with people, because it went pretty viral. It <em>still</em> gets most of the pageviews on this website.</p> <p>Well, a lot has changed in three years: I got promoted and I'm now a senior engineer and lead the web engineering discipline at <a href="https://monzo.com">Monzo</a>. But one thing hasn't changed: I still google a lot, every single day. Here's what I googled in a week, 2022 edition.</p> <p>Obvious disclaimer: this is slightly edited as I've removed most of the non-work-related ones.</p> <p>Some of these search terms might make you laugh and think &quot;how did you not know that?&quot;. Well, there are several reasons you might not know something (choose all that apply):</p> <ul> <li>you've never used it before</li> <li>you've used it before but can't remember</li> <li>it's changed since the last time you used it</li> <li>you're tired</li> <li>you're distracted</li> <li>you're human</li> </ul> <p>More than ever, we're constantly surrounded by new information. It's impossible to remember everything all of the time.</p> <p>I hope this makes you feel a bit better if you ever feel bad that you have to google something &quot;obvious&quot;.</p> <h2 id="monday" tabindex="-1">Monday</h2> <p><em>slack channel bookmarks</em> – trying to find some documentation on where to find channel bookmarks on mobile, as my friend couldn't find them.</p> <p><em>carbonara</em> – my manager made the very lofty claim in standup that his carbonara was the best, so I googled a picture to make a &quot;carbonara king&quot; emoji of him</p> <p><em>directory tree cli</em> - how to render a directory tree in a CLI</p> <p><em>react-toastify</em> - a useful notification library</p> <p><em>anchor dataset</em> - accessing data attributes in JS</p> <p><em>mdn element dataset</em></p> <p><em>waitfornextupdate</em> - trying to remember if this testing-library function was just for hooks. (It is.)</p> <p>The next few are a direct result of the fact that in JS you still can't automatically download a file in a cross-browser way without creating an anchor element. There's the <a href="https://developer.mozilla.org/en-US/docs/Mozilla/Add-ons/WebExtensions/API/downloads/download">downloads API</a>, but Safari doesn't support it.</p> <p><em>jest how to test detached elements</em> – <code>jest-dom</code> can only see what's in the DOM under test, so if an element is detached from the DOM, it's not going to be able to test it. But this didn't immediately occur to me.</p> <p><em><code>document.createElement('a')</code></em></p> <p><em><code>document.createElement('a')</code> detached</em></p> <p><em>react-hook-form</em> - we're trying to add this to some of our web properties, as they don't have any form management and <code>react-hook-form</code> is a really nice abstraction.</p> <p><em>spread types may only be created from object types</em> - I kept getting this error and I couldn't work out why, and it turns out we had an old version of <code>react-hook-form</code> installed in a different app (just monorepo things) which had different return types</p> <p><em>react-hook-form &quot;spread types may only be created from object types&quot;</em></p> <h2 id="tuesday" tabindex="-1">Tuesday</h2> <p><em>slack channel links in API</em></p> <p><em>gilts</em> – they were <a href="https://www.ftadviser.com/investments/2022/10/10/boe-to-expand-gilt-intervention-as-obr-confirms-forecast-date/">in the news</a> and I didn't actually know what they were.</p> <p><em>js accessing nested object key</em> – I momentarily couldn't remember whether you could access nested object keys like <code>myObj['key1.key2']</code>. (you can't, because the entire string is interpreted as one key)</p> <p><em>intensifies emoji maker</em> – I went with <a href="https://makeemoji.com/">MakeEmoji</a> which has a pleasing number of options</p> <p><em>modal accessibility</em> – I was having a discussion with someone about why modals aren't accessible. It turns out there is some nuance to it, and that they aren't necessarily completely inaccessible, but they require a fair bit of work to make them accessible.</p> <p><em>modal window accessibility</em></p> <p>I hope you can feel the frustration in the next few search queries, in which I struggle with Storybook and then find that my problem isn't actually documented. I knew that there was a world in which you can write JSDoc comments above a component, and have them show up in Storybook docs, because we did that at my previous job. The docs don't seem to mention this at all, and it wasn't working for some reason. It turns out that it doesn't work with default export components, for some reason, but this isn't documented anywhere.</p> <p><em>storybook docs markdown comments</em></p> <p><em>storybook story description</em></p> <p><em>storybook docs</em></p> <p><em>storybook comments</em></p> <p><em>storybook comment</em></p> <p><em>storybook JSdoc</em></p> <h2 id="wednesday" tabindex="-1">Wednesday</h2> <p><em>mark hoppus</em> – I mentioned that I was trying to get blink-182 tickets (I failed) and we were debating how old they are now.</p> <p><em>lofi girl</em> – during retro we put music on while people are writing their Retrium tickets, and I thought this was the appropriate vibe.</p> <p><em>apollo query oncompleted</em></p> <p><em>useLazyQuery</em> – trying to figure out if this returns the return value, or <code>void</code>. Turns out the latest version returns the return value, and the version we're on doesn't :(</p> <p><em>sentry github</em></p> <p><em>'RequestSessionStatus' is not exported from '@sentry/types'</em> – this turned out to not be a problem with the Sentry lib, but actually a yarn.lock mismatch (just monorepo things).</p> <h2 id="thursday" tabindex="-1">Thursday</h2> <p><em>document.write</em> – I knew this was deprecated, but I wanted to find something to back up my PR comment.</p> <p><em>apollo server request size</em> – Getting 412s from our apollo server and trying to figure out how to bump the max request size.</p> <p><em>apollo-server-koa</em></p> <p><em>apollo server request size site:stackoverflow.com</em> - a useful tip for googling is that you can restrict searches to a particular site using the <code>site:</code> param.</p> <p><em>mdn element dataset</em></p> <p><em>open new window and set inner html</em> – trying to find a suitable replacement for <code>document.write</code> for the person who requested it. We went for <code>window.open()</code> and <code>myWindow.document.documentElement.innerHTML = myHtml</code>.</p> <p><em>mock clock golang</em> – figuring out the best way to mock the <code>time.Now()</code> function in Go. There are various ways of doing it across our codebase, I wasn't sure which one was the most up-to-date way, but I figured it out eventually.</p> <p><em>graphql server request entity too large</em> – still trying to fix the apollo server request limit</p> <p><em>doggo ipsum</em> – my <a href="https://doggoipsum.com">favourite ipsum generator</a>.</p> <p><em>apollo server body parser config</em></p> <p><em>apollo server body parser config koa</em></p> <p><em>koa</em> - I finally realised we need to set the body parser config on the server itself, not the apollo-server wrapper.</p> <p><em>koa request size</em></p> <p><em>britney spears albums</em> – trying to find as many Britney track titles as possible to cram into a pun-filled gratitude post for my colleague who helped us out.</p> <p><em>ronseal</em> – I like to google pictures of <a href="https://ronseal.com">Ronseal</a> and put them in PR descriptions when the title describes exactly what the PR does (i.e. it <a href="https://en.wikipedia.org/wiki/Does_exactly_what_it_says_on_the_tin">does what it says on the tin</a>). I think I got this habit from <a href="https://b3ta.com/dictionary/define/Ronseal/">b3ta</a> back in the day.</p> <h2 id="friday" tabindex="-1">Friday</h2> <p><em>read a symbolic link</em> – I'm so shit at symbolic links lolol</p> <p><em>npm service status</em></p> <p><em>apollo async oncompleted</em></p> <p><em>window.history</em> – looking for the arguments for <code>history.push</code>.</p> <p>I think the next few are particularly amusing given that I <em>literally gave a conference talk</em> on <code>redux-saga</code> in 2018, but it's been so long since I touched any code containing sagas that I completely forgot how they work:</p> <p><em>saga execute non-saga function</em></p> <p><em>saga execute non-redux function</em></p> <p><em>redux-saga effects</em></p> <p>More ipsum generators, because my colleagues enjoyed doggo ipsum:</p> <p><em><a href="https://cupcakeipsum.com/">cupcake ipsum</a></em></p> <p><em><a href="https://www.loremipsums.nl/lorem-ipsum-origineel/veggie-ipsum/">veggie ipsum</a></em> – I <em>love</em> that I also googled this in the previous post as well, I promise it's a coincidence</p> <p><em>ts-command-line-args</em> - onto some CLI building now!</p> <p><em>node.js interactive shell</em></p> <p><em>node.js interactive shell select</em> – I was looking for a CLI library that lets you select different options with the arrow keys. I found <a href="https://github.com/SBoudrias/Inquirer.js">inquirer.js</a>!</p> <p><em>types/inquirer</em></p> <p><em>ts-command-line-args</em></p> <p><em>should you symlink from destination</em> – I can never remember what order to do symlinks in</p> <p><em>TS2464</em></p> <p><em>next-images</em> - checking that some of our plugins are still relevant.</p> <p><em>nextjs document</em></p> <p><em>react-hook-form radio buttons</em> - sometimes you just need a good example. Turns out it was easier than I thought, and I just needed to forward a ref to our radio button component.</p> <h2 id="looking-back" tabindex="-1">Looking back</h2> <p>You might say &quot;well, you googled fewer things this week than you did that week in 2019!&quot;. For one thing, I have more meetings now than I did back then. I'm in a different team, working on different things.</p> <p>It also depends from week to week what I'm working on; last week I spent a lot of time building data export in Go, and so my search history was full of frustrated queries like <code>golang readseeker from buffer</code> and <code>create a file from string golang</code>. (I still help out on backend tickets when it's needed.)</p> <p>Some of the stuff I googled back then I can remember how to do without looking it up now, but some of it I definitely can't (e.g. I still can't get my brain to retain some more complex CSS grid things). For example, I'd 100% still have to google all of these from the last post:</p> <p><em>whitespace regex</em></p> <p><em>regex not letter</em></p> <p><em>js date</em></p> <p><em>grid minmax</em></p> <p>There you have it – I still google loads of stuff. To finish, I'll leave you with what I said in the post from 2019:</p> <blockquote> <p>What I'm trying to show with all this is that you can do something 100 times but still not remember how to do it off the top of your head. Never be ashamed of googling, even if it seems like the most basic thing you're looking up.</p> </blockquote> When going back doesn't mean going backwards 2022-08-27T00:00:00Z https://localghost.dev/blog/when-going-back-doesnt-mean-going-backwards/ <p>Hey internet! Last time we spoke, I was in a bad place. Burnt out and off work. Well, in May I made the decision to quit my job at the startup after 7 months, and in June I accepted a new job at... my old job. So I'm back at Monzo, <a href="https://twitter.com/type__error/status/1554859381875003393">working at the bank</a>.</p> <p>You don't often hear about people going back to jobs they've left. It turns out it's really nice, because you already have friends there and people are happy to see you come back. For me, leaving Monzo was always a pull, not a push: this exciting opportunity came up at <a href="http://incident.io">incident.io</a>, and I thought I'd give it a go.</p> <p>Accepting that a job isn't working out can be a difficult pill to swallow. I really wanted to make it work, and I think a lot of the burnout I experienced was because it wasn't the right job for me. Being part of an early-stage startup seemed to be the hot thing to do, and this particular one is run by people I really respect. I had to come to terms with the fact that early-stage startups aren't really my cup of tea: I realised that I work best in bigger organisations, where I can take more of a leadership role and do larger-scale work that improves things for the people around me.</p> <p>I also realised that I didn't really want to market myself as a full-stack developer any more. The week I got back from my time off, I went to <a href="https://heypresents.com/conferences/2022">All Day Hey</a> in Leeds and I left with this incredible sense of clarity. It reminded me of everything I love about the web. And it made me finally accept the fact that, well, I don't like backend development very much.</p> <p>See, I've always had a degree of internalised self-doubt about being web developer, as if it means I'm somehow not a &quot;proper&quot; developer for not being a backend dev. (Never mind that if anyone said that to me, I'd give them an earful.)</p> <p>I've been building websites for over 20 years, but when I learnt Java during my masters I thought I'd become a backend dev, because that was what Real Programmers do. After my career took me onto a React/Node project at John Lewis, then joining Monzo as a full-stack web engineer, I clung on to the &quot;full-stack&quot; title. Doing backend too meant that I could still consider myself a Serious Dev while still doing the fun web stuff as well.</p> <p>That &quot;fun web stuff&quot; included teaching people about accessibility, improving testing, architecting and building out brand new web apps, upgrading countless libraries, implementing microfrontends in a large React monolith, architecting and building component libraries. It involved becoming recognised as a conference speaker, and being invited to conferences about web development in the UK and abroad. It involved being promoted to Web Discipline Lead at Monzo, where I could be the &quot;public face&quot; of web internally at Monzo, defining and introducing engineering standards across the organisation. But it's okay because I was still doing backend engineering to make me a Proper Developer...!</p> <p>Oh dear.</p> <p>At All Day Hey, when I watched Andy Bell's brilliant talk <a href="https://heypresents.com/talks/be-the-browser-s-mentor-not-its-micromanager">Be The Browser's Mentor, Not Its Micromanager</a>, I was <em>so excited</em> by what I'd just seen. I hadn't felt that much enthusiasm about what I did for months – sorry, but the introduction of Generics in Go doesn't hold a candle to modern CSS utility functions and what they mean for responsive website design 💅</p> <p>I realised that I hadn't been doing enough of what I love, and I LOVE THE WEB. Web development is awesome. (And I <em>am</em> a Proper Developer.) Sadly the company's engineering needs didn't really match up with my specialism.</p> <p>The week after, I handed in my notice. I was sad, but we all agreed it was the right thing, and everyone was super supportive.</p> <p>Ultimately, the 7 months at the startup were well spent: this experience helped me to understand what I really wanted for my career – and I realised it was pretty much what I was doing before. Plus, I met some excellent people, and learnt a lot from them. (I also built a pretty sweet component library, and I hope they're getting good use out of it.)</p> <p>This isn't a cautionary tale: it's a reassurance that it's okay to make the wrong decision. The wrong decision is not necessarily a bad decision, and going back to my old job isn't a backwards step – I picked up where I left off and I'm heading in the same direction I was when I left. I don't have any regrets about the past year – I learnt a lot, I have a lot of love for the folks at <a href="http://incident.io">incident.io</a>, and they're building something amazing. It just wasn't the right thing for me, and that's okay.</p> Burnout, a cautionary tale (and a plea to take a break) 2022-04-16T00:00:00Z https://localghost.dev/blog/burnout-a-cautionary-tale-and-a-plea-to-take-a-break/ <p>It's Easter and I'm off work til the end of the month. It was originally just going to be for a few days as I'd used up most of my holiday already, but after I burst into tears at my manager on Wednesday during our 1:1 when he asked me how a project was going, he suggested I might need a bit longer (good manager).</p> <p>I really love my job, I love what I do and I love my colleagues. Before the pandemic - and even during it - I threw myself at my work and learnt as much as I possibly could. And from my beginnings learning Java at university, I learnt JavaScript, then React, then Kotlin. Then I moved onto Monzo and picked up Go. I was Good At Things.</p> <p>But for the last few months I've found that nothing goes in any more. I can't absorb new information. People have to explain things several times, and my head feels like it's full of cotton wool. It makes me feel like an idiot. I'd tell myself I was better than this and I needed to just <em>focus</em> and maybe I was just getting too distracted.</p> <p>When I sit down to do web development, it's like autopilot and I can steam through it, but anything backend seems to be beyond my capability at the moment. Which is frustrating because there's a lot of backend development in my current role. I used to love it, what gives? I'm out of practice, sure, but it's like there's this barrier that stops me from learning anything new.</p> <p>Weeeelllll, now I can put a name to that barrier: burnout. FFS.</p> <p>Was it my job, I thought? Was I unhappy in my new role? Well, the thought of going back to my old job didn't make me feel any better. Nor would going anywhere else. Switching to a new job certainly came with the stress of starting from scratch, and working at a startup is a whole other vibe. But ultimately I do like it, and I knew that in any other circumstances I'd be thriving there.</p> <p>So let's look at everything else.</p> <p>The last two years have been among the worst of my life. I'm sure they have for many of you, too. I spent so long unable to do some of the things I love, in a lockdown that could have been a lot shorter if our government wasn't equal parts inept and corrupt. The world is permanently on fire. I had to take a hiatus from reading the news because it was all too much. Plus I've had my own depression to contend with and other family things that I won't go into, which have made everything that much more difficult.</p> <h2 id="don-t-be-like-me-please-take-a-break" tabindex="-1">Don't be like me, please take a break</h2> <p>I wrote a blog post in 2020 about <a href="https://localghost.dev/blog/give-yourself-a-break-lessons-from-burnout/">giving yourself a break</a>, but apparently I didn't take enough of my own advice.</p> <p>For someone who goes on about work-life balance and mental health, I'm not actually very good at practising what I preach. (Surprise.)</p> <p>I'd figured that because I never work late or out-of-hours, I'd avoid burnout. But that's just one contributing factor.</p> <p>And when I take time off I usually feel like I need to fill it with Stuff. Projects, outings, day trips, holidays. Making the most of your time off. I've never been good at doing <em>nothing</em>.</p> <p>A couple of weeks ago I was ill with a mystery virus that seemed an awful lot like covid but was negative on all the tests, and a few mornings that week I'd woken up feeling loads better and started working. By the afternoon I had to give up and down tools again because I felt so ill.</p> <p>I thought I'd be making myself useful by working when I felt better, but instead I was being more disruptive than anything else. People didn't know if I was going to finish the thing I was working on, so they didn't pick it up, and stuff kind of stopped and started. If I'd just taken the rest of the week and shoved my laptop in a cupboard, I'd have actually got the rest I needed.</p> <h2 id="heed-my-words-adventurer" tabindex="-1">Heed my words, adventurer!</h2> <p>I am not writing this post to garner sympathy, I'm sharing because I want to be transparent about the fact that this doesn't &quot;just happen to other people&quot;. I have been running on fumes for months, and it culminated in me crying in a coffee shop in front of my extremely kind and patient manager.</p> <p>If you are feeling like things are harder these days, that's because they are. If you're not functioning at 100% and find you're pushing yourself even harder because of it, please stop. I want you to take a minute to check on yourself. Are you okay? Do you need to take some time off?</p> <p>(Also, never be ashamed of crying at work. We've all been there. Normalise crying at work. It's a sign that something is wrong, but the crying itself isn't anything to be ashamed of.)</p> <p>I believe in working hard, but I do not believe in working hard at the expense of your physical and mental health.</p> <p>So now I'm on a break until May, and I'm going to go for walks, go birdwatching, do jigsaws, plant some vegetables in the allotment, play video games and make a big fuss of my father-in-law's dog Boris.</p> <p>If you've got some holiday, please take it.</p> <figure><picture><source type="image/webp" srcset="https://localghost.dev/img/SSxeMwF9F3-280.webp 280w, https://localghost.dev/img/SSxeMwF9F3-593.webp 593w" sizes="auto"><img loading="lazy" decoding="async" src="https://localghost.dev/img/SSxeMwF9F3-280.jpeg" alt="A brown and white Staffordshire bull terrier lies on the sofa with his limbs extended. He is greying round the muzzle, and has what looks like a smile. He's lying next to a PS5 controller." width="593" height="581" srcset="https://localghost.dev/img/SSxeMwF9F3-280.jpeg 280w, https://localghost.dev/img/SSxeMwF9F3-593.jpeg 593w" sizes="auto"></picture><figcaption>My plans.</figcaption></figure> Start at the beginning: the importance of learning the basics 2022-01-02T00:00:00Z https://localghost.dev/blog/start-at-the-beginning-the-importance-of-learning-the-basics/ <p>If you're an early-career developer, Twitter is overflowing with people tweeting great tips – and some absolute rubbish – about how to improve your skills and become better at your job. I've spoken to more than a few people who've asked me, &quot;how should I start?&quot;. And I tell everyone the same thing: <strong>learn the basics.</strong></p> <p>However you learn best – book, video, interactive tutorial – <strong>you need to learn HTML and CSS before you can call yourself a web developer</strong>. I don't think that's a particularly controversial statement.</p> <p>Once you start getting into interactive website territory, with API calls and fancy stuff, that's where you need JavaScript (JS) knowledge. More specifically, <strong>vanilla JS</strong>: plain JS with no additional frameworks or plugins. The JS that your browser understands without having to do any pre-processing. It makes working with frameworks a whole lot easier, and it'll help you to know when <em>not</em> to use a framework (and avoid making users download massive JS bundles when all you need is a tiny bit of code). Browsers have come a <em>long</em> way, and a lot of what we might have needed to use <a href="https://babeljs.io/">Babel</a> to do even just a couple of years ago is now natively supported in the big 4 browsers (Chrome, Firefox, Edge and Safari).</p> <h2 id="where-i-started" tabindex="-1">Where I started</h2> <p>I started building websites when I was ten years old. I was lucky enough to grow up with computers in the house, and had a book called &quot;Make Your Own Webpage&quot; which taught me the basics of HTML (as it was in 1999). You can even <a href="https://archive.org/details/makeyourownwebpa00pede">read it on archive.org</a>.</p> <figure><picture><source type="image/webp" srcset="https://localghost.dev/img/x_sXCe4Q1y-280.webp 280w, https://localghost.dev/img/x_sXCe4Q1y-640.webp 640w, https://localghost.dev/img/x_sXCe4Q1y-823.webp 823w" sizes="auto"><img loading="lazy" decoding="async" src="https://localghost.dev/img/x_sXCe4Q1y-280.jpeg" alt="The front cover of the book 'Make Your Own Webpage, a guide for kids, from the Creators of Internet for Kids!' by Ted Pedersen and Francis Moss. The cover features a CRT monitor and beige desktop computer with a computer mouse that looks like a real mouse, on a mousemat that looks like a slice of cheese." width="823" height="1024" srcset="https://localghost.dev/img/x_sXCe4Q1y-280.jpeg 280w, https://localghost.dev/img/x_sXCe4Q1y-640.jpeg 640w, https://localghost.dev/img/x_sXCe4Q1y-823.jpeg 823w" sizes="auto"></picture></figure> <p>A few years later, I built my personal website full of webrings and stolen gifs with the help of <a href="http://lissaexplains.com/">Lissa Explains It All</a>, the ultimate HTML bible for anyone on the internet in 2002. (It's still online today, but pretty out of date!)</p> <p>Fast-forward through LiveJournal, Greymatter, Blogger, Wordpress, pretty much any pre-2010 blogging site/CMS framework you can imagine – and now I'm building websites (or web apps) as my actual job, using React. A fair bit has changed – but not as much as you'd think.</p> <p>I'm telling you this because I <strong>still use the knowledge I learned when I was ten</strong> every day at work. HTML tags are still HTML tags, they're still written much the same way – HTML5 got rid of some of the ones I used to use (pour one out for <code>marquee</code>) and introduced some new ones, but the fundamentals are still the same. Most of the websites that I built in 1999 would still render today.</p> <p>My first professional web development experience was a React app. I'd just about heard of React at that point, but I'd recently learned JavaScript and Node.JS which gave me a good foundation to build upon. I brushed up on some newer CSS concepts I hadn't used, but all the HTML and CSS of my past web adventures came right back. The good thing about this stuff is that it's all over the internet, so when you inevitably forget how to do something really simple, you can just google it.</p> <h2 id="frameworks-serve-a-purpose-but-they-don-t-replace-the-basics" tabindex="-1">Frameworks serve a purpose, but they don't replace the basics</h2> <p>People might tell you to learn React, because everyone's using React. I write React every day, and a lot of that is just JavaScript and HTML. If you don't know either of those, you're going to have a bad time – or you'll end up with highly specialised framework-specific knowledge that will bite you later on when you need to use a different framework. Lots of people are using React, sure – but a lot more people aren't. A far, far larger proportion of the web runs on Wordpress, for example. Lots of sites are built in plain old HTML, CSS and JS, like the <a href="https://emojinator.fun">emojinator</a> and this very website you're reading.</p> <p>Besides, in a few years we'll probably using the newest, hottest framework and React will become the butt of jokes in the way that Angular has. Or you'll land a job at your dream company and find they use Vue, or some state management library from 2011 which has no documentation. I'm not saying don't learn React – I'm saying learn the basics first, so you can apply your knowledge to all manner of other frameworks and tools.</p> <p>The same goes for CSS. There was a particularly bad take floating around Twitter recently where someone suggested that junior developers should skip CSS and just learn <a href="https://tailwindcss.com/">Tailwind</a>. I have used Tailwind, and I dislike it for various reasons, but I can see use cases for it such as quick prototyping and a set of nice defaults in the absence of specialist design. However, it doesn't and shouldn't &quot;replace&quot; CSS. Tailwind is literally just a library of someone else's CSS classes, except you have a separate class for nearly every rule instead of writing all your rules together in reusable classes for each component. Instead of learning hundreds of class names, why not learn the CSS rules they translate to and write one CSS class with multiple selectors? When you go to your next job or project and find they don't use Tailwind, it'll slow you down as you need to learn the selectors all over again.</p> <h2 id="recommended-resources" tabindex="-1">Recommended resources</h2> <p>I'd recommend going in the order HTML, CSS, JS. That way, you can build something in HTML, add CSS to it as you learn it, and finally soup it up with your new-found JS knowledge.</p> <p>If you're a backend engineer who only touches the frontend occasionally, you don't need to go too deep – but you need a decent grasp of which element does what (<a href="https://localghost.dev/0221/06/the-right-tag-for-the-job-why-you-should-use-semantic-html/">semantic HTML</a>) so you can make sure you're using the right elements and not creating any accessibiliy problems.</p> <h3 id="learning-html" tabindex="-1">Learning HTML</h3> <p>I usually recommend these tutorials which provide a really nice overview of different types of elements. Try using some of the tags in <a href="https://codepen.io/pen/">Codepen</a>, and watch things render before your eyes.</p> <ul> <li><a href="https://developer.mozilla.org/en-US/docs/Learn/Getting_started_with_the_web/HTML_basics">Getting started with HTML - MDN</a></li> <li><a href="https://.freecodecamp.org/learn/responsive-web-design/#basic-html-and-html5">FreeCodeCamp's Responsive Web Design course - Basic HTML and HTML5</a></li> </ul> <p>Learn from how other people do it. Web pages are so bloated with scripts and analytics these days that it's hard to just right-click and &quot;View Source&quot; in the same way that I did back in the early 00s – I learnt a lot of HTML by nicking other people's code! But you can use browser dev tools to look at the structure of the page (the Document Object Model or DOM, as it's known):</p> <ul> <li><a href="https://developer.chrome.com/docs/devtools/open/#elements">Chrome dev tools</a></li> <li><a href="https://developer.mozilla.org/en-US/docs/Tools/Page_Inspector">Firefox inspector</a></li> </ul> <h3 id="learning-css" tabindex="-1">Learning CSS</h3> <p><a href="https://codepen.io/pen/">Codepen</a> comes in useful again here to easily write and apply CSS styles to HTML, and have it update instantly.</p> <p>Again, MDN has a great beginners' guide. CSS Tricks is my favourite place for useful CSS knowledge (I referred back to their Flexbox cheatsheet for YEARS) and really clearly written guides.</p> <p><strong>Free</strong></p> <ul> <li><a href="https://developer.mozilla.org/en-US/docs/Learn/Getting_started_with_the_web/CSS_basics">CSS basics – MDN</a></li> <li><a href="https://css-tricks.com/almanac/">CSS Almanac – CSS Tricks</a> – a guide to nearly every selector and property in CSS</li> <li><a href="https://css-tricks.com/guides/">CSS Tricks guides</a> for in-depth info</li> </ul> <p><strong>Paid</strong></p> <ul> <li><a href="https://egghead.io/courses/css-fundamentals">CSS fundamentals on egghead.io</a> by Tyler Clark (video)</li> </ul> <h3 id="learning-java-script" tabindex="-1">Learning JavaScript</h3> <p>You want to start with vanilla JS. Having a solid foundation in that will make it easier to use frameworks and libraries in the future.</p> <p>How in-depth you go is up to you, and depends on how much interactive stuff you want to build on your website. If you just want a plain static site with a bit of markdown, you probably don't need JavaScript. This website has a tiny bit of JS on that controls dark mode; without that, it'd just be HTML and CSS (though I did use a static site generator to build it).</p> <p>Besides the obligatory MDN article, here are some good resources as of 2022:</p> <p><strong>Free</strong></p> <ul> <li><a href="https://developer.mozilla.org/en-US/docs/Learn/Getting_started_with_the_web/JavaScript_basics">Getting started with JavaScript – MDN</a></li> <li><a href="https://freecodecamp.org">FreeCodeCamp</a></li> <li><a href="https://javascript.info/">javascript.info</a></li> <li><a href="https://wesbos.com/javascript">Wes Bos's Beginner JS Notes &amp; Reference</a></li> <li><a href="https://javascript30.com/">Wes Bos – 30 Day Vanilla JS Coding Challenge</a></li> </ul> <p><strong>Paid</strong></p> <ul> <li><a href="https://vanillajsacademy.com/essentials/">Vanilla JS Academy</a> (structured course)</li> </ul> The right tag for the job: why you should use semantic HTML 2021-06-06T00:00:00Z https://localghost.dev/blog/the-right-tag-for-the-job-why-you-should-use-semantic-html/ <p>I've come across a lot of websites in my career (and in daily browsing) that are straight-up inaccessible. If you've ever worked on a project that is riddled with accessibility issues, you'll know that fixing these problems is a mammoth task - it needs <em>time</em>, it needs <em>people</em>, it needs <em>prioritisation</em>... it costs money, basically. As I mentioned in my blog post <a href="https://localghost.dev/2020/10/7-myths-designers-and-developers-believe-about-web-accessibility/">7 myths designers and developers believe about web accessibility</a>, retrofitting accessibility is <em>hard</em>.</p> <p>The key is to <em>start out</em> with the right tools for the job - or rather, the right <em>tags</em> for the job. In every website, underneath the layers of CSS, the bloated frameworks, the 45945 API calls, there's good old HTML, and that's the key to accessible websites. Specifically, <b>semantic HTML</b>.</p> <h2 id="semantic-html-conveys-meaning" tabindex="-1">Semantic HTML conveys meaning</h2> <p>Semantics is the study of meaning, and semantic HTML is exactly that - HTML tags that convey meaning. They tell the browser - and assistive technology such as screenreaders - about the structure of the page, and how it should behave when you interact with those elements. A button should let you click on it. An ordered list should have numbers.</p> <p>Sighted people rely on visual information to tell them about the structure of a page. Based on our prior knowledge of UI conventions, we can identify where a header is, which parts of the page are buttons or form elements, and what the title of the page is, for example. But if you don't get that visual information, how can you tell what's on the page?</p> <p>Assistive technology such as screenreaders will take the HTML markup of the page and present it to the user either by reading it out audibly, or sending the output to something like a <a href="https://en.wikipedia.org/wiki/Refreshable_braille_display">braille display</a>. They'll use the different types of HTML tags in the document to present structured information to the user, allowing them to navigate by headings, or cycle through the links in the page. So it matters what HTML tags we use, to make sure the screenreaders get the complete picture.</p> <h2 id="example-a-non-semantic-news-site" tabindex="-1">Example: a non-semantic news site</h2> <p>Take this mocked-up news site (as you can see, graphic design is my passion). I've built it using mostly <code>&lt;div&gt;</code> elements, with a couple of <code>&lt;h1&gt;</code>s as I wanted some nice big headings. My menu at the top is a series of <code>&lt;div&gt;</code>s containing links, and the footer at the bottom is the same.</p> <p class="codepen" data-height="265" data-theme-id="dark" data-default-tab="result" data-user="sophiekoonin" data-slug-hash="jOBxyVB" data-preview="true" data-pen-title="Semantic HTML blog post example: no semantics"> <span>See the Pen <a href="https://codepen.io/sophiekoonin/pen/jOBxyVB"> Semantic HTML blog post example: no semantics</a> by <a href="https://codepen.io/sophiekoonin">@sophiekoonin</a> on <a href="https://codepen.io">CodePen</a>.</span> </p> <script async="" src="https://cpwebassets.codepen.io/assets/embed/ei.js"></script> <p>If you're a sighted user like me, it looks perfectly fine. I can see a menu, a header, some titles and a footer. The byline and image caption is nicely italicised, and there are some nice buttons and fancy SVG checkboxes on the cookie banner.</p> <p>But let's take a look at this page from the perspective of assistive tech, which relies on the underlying HTML markup to establish the structure of the document. I'm using the screenreader software <a href="https://support.apple.com/en-gb/guide/voiceover/welcome/mac">VoiceOver</a>, which is built into MacOS, but if you're on Windows you can use <a href="https://www.nvaccess.org/download/">NVDA</a> which is free to download.</p> <h3 id="landmarks" tabindex="-1">Landmarks</h3> <p>Screenreader software such as VoiceOver, JAWS and NVDA provides a way for users to navigate to different sections on the page based on <b>landmarks</b>. So, you could skip directly to the footer, or directly to the main part of the site. This relies on these landmarks being signposted through semantic tags such as <code>&lt;header&gt;</code>,<code>&lt;main&gt;</code> and <code>&lt;footer&gt;</code>. We haven't used any of these tags in the example, so screen readers wouldn't be able to identify any landmarks.</p> <p>When I run VoiceOver and press Ctrl+Option+Shift+I to tell me about the page, it says:</p> <blockquote> <p>Page contains 14 links 2 headings</p> </blockquote> <p>When I properly mark up the header, footer and main sections of the page, with an <code>&lt;article&gt;</code> around the content of the news article itself, VoiceOver allows me to navigate between them with Ctrl+Option+right/left arrow. It tells me:</p> <blockquote> <p>Page contains 14 links 2 headings 3 landmarks 1 article</p> </blockquote> <h3 id="menus" tabindex="-1">Menus</h3> <p>If a screenreader user is trying to find the menu in this news page, they're going to have to go through all the links on the page to try and find one that seems like it could be a menu link. The <code>&lt;div&gt;</code> wrapping the menu is completely ignored by the screenreader, so all they hear is each individual link. There's nothing to tell them which part is the menu. People shouldn't have to tab through the page incessantly to find out how to navigate to a different part of the website.</p> <p>Prior to HTML5, the best way to mark up menus with lists of links was an unordered list - <code>&lt;ul&gt;</code>. Now we have the <code>&lt;nav&gt;</code> tag, short for navigation.</p> <pre class="language-html"><code class="language-html"><span class="token tag"><span class="token tag"><span class="token punctuation">&lt;</span>nav</span><span class="token punctuation">></span></span> <span class="token tag"><span class="token tag"><span class="token punctuation">&lt;</span>ul</span> <span class="token attr-name">class</span><span class="token attr-value"><span class="token punctuation attr-equals">=</span><span class="token punctuation">"</span>menu<span class="token punctuation">"</span></span><span class="token punctuation">></span></span> <span class="token tag"><span class="token tag"><span class="token punctuation">&lt;</span>li</span><span class="token punctuation">></span></span><span class="token tag"><span class="token tag"><span class="token punctuation">&lt;</span>a</span> <span class="token attr-name">href</span><span class="token attr-value"><span class="token punctuation attr-equals">=</span><span class="token punctuation">"</span>#<span class="token punctuation">"</span></span><span class="token punctuation">></span></span>News<span class="token tag"><span class="token tag"><span class="token punctuation">&lt;/</span>a</span><span class="token punctuation">></span></span><span class="token tag"><span class="token tag"><span class="token punctuation">&lt;/</span>li</span><span class="token punctuation">></span></span> <span class="token tag"><span class="token tag"><span class="token punctuation">&lt;</span>li</span><span class="token punctuation">></span></span><span class="token tag"><span class="token tag"><span class="token punctuation">&lt;</span>a</span> <span class="token attr-name">href</span><span class="token attr-value"><span class="token punctuation attr-equals">=</span><span class="token punctuation">"</span>#<span class="token punctuation">"</span></span><span class="token punctuation">></span></span>Politics<span class="token tag"><span class="token tag"><span class="token punctuation">&lt;/</span>a</span><span class="token punctuation">></span></span><span class="token tag"><span class="token tag"><span class="token punctuation">&lt;/</span>li</span><span class="token punctuation">></span></span> <span class="token tag"><span class="token tag"><span class="token punctuation">&lt;</span>li</span><span class="token punctuation">></span></span><span class="token tag"><span class="token tag"><span class="token punctuation">&lt;</span>a</span> <span class="token attr-name">href</span><span class="token attr-value"><span class="token punctuation attr-equals">=</span><span class="token punctuation">"</span>#<span class="token punctuation">"</span></span><span class="token punctuation">></span></span>World<span class="token tag"><span class="token tag"><span class="token punctuation">&lt;/</span>a</span><span class="token punctuation">></span></span><span class="token tag"><span class="token tag"><span class="token punctuation">&lt;/</span>li</span><span class="token punctuation">></span></span> <span class="token tag"><span class="token tag"><span class="token punctuation">&lt;</span>li</span><span class="token punctuation">></span></span><span class="token tag"><span class="token tag"><span class="token punctuation">&lt;</span>a</span> <span class="token attr-name">href</span><span class="token attr-value"><span class="token punctuation attr-equals">=</span><span class="token punctuation">"</span>#<span class="token punctuation">"</span></span><span class="token punctuation">></span></span>Sport<span class="token tag"><span class="token tag"><span class="token punctuation">&lt;/</span>a</span><span class="token punctuation">></span></span><span class="token tag"><span class="token tag"><span class="token punctuation">&lt;/</span>li</span><span class="token punctuation">></span></span> <span class="token tag"><span class="token tag"><span class="token punctuation">&lt;</span>li</span><span class="token punctuation">></span></span><span class="token tag"><span class="token tag"><span class="token punctuation">&lt;</span>a</span> <span class="token attr-name">href</span><span class="token attr-value"><span class="token punctuation attr-equals">=</span><span class="token punctuation">"</span>#<span class="token punctuation">"</span></span><span class="token punctuation">></span></span>Tech<span class="token tag"><span class="token tag"><span class="token punctuation">&lt;/</span>a</span><span class="token punctuation">></span></span><span class="token tag"><span class="token tag"><span class="token punctuation">&lt;/</span>li</span><span class="token punctuation">></span></span> <span class="token tag"><span class="token tag"><span class="token punctuation">&lt;/</span>ul</span><span class="token punctuation">></span></span> <span class="token tag"><span class="token tag"><span class="token punctuation">&lt;/</span>nav</span><span class="token punctuation">></span></span></code></pre> <p>Technically, you don't have to have a list inside the <code>&lt;nav&gt;</code> - you can just put links in. I personally like to keep the list inside - the nice thing about lists is that some screen readers will read out how many items there are, so the user knows how long to keep tabbing through. It's also good for backwards compatibility in case any older screen readers don't understand <code>&lt;nav&gt;</code>.</p> <p>Lists can be styled easily (including removing bullet points), and <code>&lt;li&gt;</code> (list item) elements can contain links and buttons.</p> <p>You can add <code>aria-label</code> attributes to your <code>&lt;nav&gt;</code> elements to give even more information about the purpose of the menu.</p> <pre class="language-html"><code class="language-html"><span class="token tag"><span class="token tag"><span class="token punctuation">&lt;</span>nav</span> <span class="token attr-name">aria-label</span><span class="token attr-value"><span class="token punctuation attr-equals">=</span><span class="token punctuation">"</span>main navigation<span class="token punctuation">"</span></span><span class="token punctuation">></span></span></code></pre> <p>Here's how VoiceOver interprets all of this:</p> <blockquote> <p>main navigation, navigation</p> <p>list, 5 items</p> <p>link, Sport, 4 of 5</p> </blockquote> <p>Whether you use <code>&lt;nav&gt;</code> or not isn't a hard-and-fast rule. I noted that <a href="https://gov.uk">gov.uk</a> (an example of amazing accessible design) doesn't actually use them at all, sticking with unordered lists. As long as you use one and/or the other, you'll be fine.</p> <p>As an aside, I recommend using the Dev Tools inspector to look through the source for <a href="http://gov.uk">gov.uk</a> to see how they use ARIA attributes, as they use them very well.</p> <h3 id="checkboxes" tabindex="-1">Checkboxes</h3> <p>These checkboxes aren't real checkboxes - they're <code>&lt;div&gt;</code> elements with CSS <code>:before</code> pseudoselectors containing an SVG checkbox icon. VoiceOver reads them out as plain text, leaving you with no idea you can even click them.</p> <blockquote> <p>Chocolate chip</p> </blockquote> <p>It's super annoying that you can't style the default checkbox in browsers, but there are ways around it that still let you use <code>&lt;input type=&quot;checkbox&quot;&gt;</code>. The same goes for radio buttons.</p> <p>If we put the input back in and <em>visually</em> hide it (leaving the input element in the DOM), we get a much more helpful readout:</p> <pre class="language-html"><code class="language-html"> <span class="token tag"><span class="token tag"><span class="token punctuation">&lt;</span>label</span> <span class="token attr-name">for</span><span class="token attr-value"><span class="token punctuation attr-equals">=</span><span class="token punctuation">"</span>choc-chip<span class="token punctuation">"</span></span><span class="token punctuation">></span></span> <span class="token tag"><span class="token tag"><span class="token punctuation">&lt;</span>input</span> <span class="token attr-name">id</span><span class="token attr-value"><span class="token punctuation attr-equals">=</span><span class="token punctuation">"</span>choc-chip<span class="token punctuation">"</span></span> <span class="token attr-name">class</span><span class="token attr-value"><span class="token punctuation attr-equals">=</span><span class="token punctuation">"</span>visually-hidden<span class="token punctuation">"</span></span> <span class="token attr-name">type</span><span class="token attr-value"><span class="token punctuation attr-equals">=</span><span class="token punctuation">"</span>checkbox<span class="token punctuation">"</span></span> <span class="token punctuation">/></span></span> <span class="token tag"><span class="token tag"><span class="token punctuation">&lt;</span>div</span> <span class="token attr-name">class</span><span class="token attr-value"><span class="token punctuation attr-equals">=</span><span class="token punctuation">"</span>pretty-checkbox<span class="token punctuation">"</span></span> <span class="token attr-name">aria-hidden</span><span class="token attr-value"><span class="token punctuation attr-equals">=</span><span class="token punctuation">"</span>true<span class="token punctuation">"</span></span><span class="token punctuation">></span></span><span class="token tag"><span class="token tag"><span class="token punctuation">&lt;/</span>div</span><span class="token punctuation">></span></span> Chocolate chip <span class="token tag"><span class="token tag"><span class="token punctuation">&lt;/</span>label</span><span class="token punctuation">></span></span></code></pre> <p>(NB. you could even just have the actual SVG inline here)</p> <blockquote> <p>Chocolate chip, unticked, checkbox</p> </blockquote> <p>We hide the &quot;pretty&quot; checkbox from screen readers, because they will pick up the <code>&lt;input&gt;</code>.</p> <p>The input itself has a CSS class of <code>&quot;visually-hidden&quot;</code>, which looks like this:</p> <pre class="language-css"><code class="language-css"><span class="token selector">.visually-hidden</span> <span class="token punctuation">{</span> <span class="token property">display</span><span class="token punctuation">:</span> block<span class="token punctuation">;</span> <span class="token property">height</span><span class="token punctuation">:</span> 1px<span class="token punctuation">;</span> <span class="token property">width</span><span class="token punctuation">:</span> 1px<span class="token punctuation">;</span> <span class="token property">overflow</span><span class="token punctuation">:</span> hidden<span class="token punctuation">;</span> <span class="token property">clip</span><span class="token punctuation">:</span> <span class="token function">rect</span><span class="token punctuation">(</span>1px 1px 1px 1px<span class="token punctuation">)</span><span class="token punctuation">;</span> <span class="token property">clip</span><span class="token punctuation">:</span> <span class="token function">rect</span><span class="token punctuation">(</span>1px<span class="token punctuation">,</span> 1px<span class="token punctuation">,</span> 1px<span class="token punctuation">,</span> 1px<span class="token punctuation">)</span><span class="token punctuation">;</span> <span class="token property">clip-path</span><span class="token punctuation">:</span> <span class="token function">inset</span><span class="token punctuation">(</span>1px<span class="token punctuation">)</span><span class="token punctuation">;</span> <span class="token property">white-space</span><span class="token punctuation">:</span> nowrap<span class="token punctuation">;</span> <span class="token property">position</span><span class="token punctuation">:</span> absolute<span class="token punctuation">;</span> <span class="token punctuation">}</span></code></pre> <p>We can't simply use <code>visibility: hidden</code> or <code>display: none</code> as these attributes actually apply to screenreaders as well. Anything hidden that way will also be hidden to screenreaders. This <code>visually-hidden</code> class makes sure that elements with this styling are invisible to sighted users, but still visible to assistive tech.</p> <p>For more information on this approach, check out <a href="https://www.sarasoueidan.com/blog/inclusively-hiding-and-styling-checkboxes-and-radio-buttons/">Sara Soueidan's guide to inclusively hiding &amp; styling checkboxes and radio buttons</a>.</p> <p>For even more semantic goodness, we can now wrap our checkboxes in a <code>&lt;fieldset&gt;</code>, which groups together related form-fields. We can then attach the &quot;Which of these cookies can we use?&quot; text to the fieldset by making it a <code>&lt;legend&gt;</code>:</p> <pre class="language-html"><code class="language-html"><span class="token tag"><span class="token tag"><span class="token punctuation">&lt;</span>fieldset</span><span class="token punctuation">></span></span> <span class="token tag"><span class="token tag"><span class="token punctuation">&lt;</span>legend</span><span class="token punctuation">></span></span>Which of these cookies can we use?<span class="token tag"><span class="token tag"><span class="token punctuation">&lt;/</span>legend</span><span class="token punctuation">></span></span> <span class="token tag"><span class="token tag"><span class="token punctuation">&lt;</span>label</span> <span class="token attr-name">for</span><span class="token attr-value"><span class="token punctuation attr-equals">=</span><span class="token punctuation">"</span>choc-chip<span class="token punctuation">"</span></span><span class="token punctuation">></span></span> <span class="token tag"><span class="token tag"><span class="token punctuation">&lt;</span>input</span> <span class="token attr-name">id</span><span class="token attr-value"><span class="token punctuation attr-equals">=</span><span class="token punctuation">"</span>choc-chip<span class="token punctuation">"</span></span> <span class="token attr-name">type</span><span class="token attr-value"><span class="token punctuation attr-equals">=</span><span class="token punctuation">"</span>checkbox<span class="token punctuation">"</span></span> <span class="token punctuation">/></span></span> <span class="token tag"><span class="token tag"><span class="token punctuation">&lt;</span>div</span> <span class="token attr-name">class</span><span class="token attr-value"><span class="token punctuation attr-equals">=</span><span class="token punctuation">"</span>pretty-checkbox<span class="token punctuation">"</span></span> <span class="token attr-name">aria-hidden</span><span class="token attr-value"><span class="token punctuation attr-equals">=</span><span class="token punctuation">"</span>true<span class="token punctuation">"</span></span><span class="token punctuation">></span></span><span class="token tag"><span class="token tag"><span class="token punctuation">&lt;/</span>div</span><span class="token punctuation">></span></span> Chocolate chip <span class="token tag"><span class="token tag"><span class="token punctuation">&lt;/</span>label</span><span class="token punctuation">></span></span> <span class="token tag"><span class="token tag"><span class="token punctuation">&lt;</span>label</span> <span class="token attr-name">for</span><span class="token attr-value"><span class="token punctuation attr-equals">=</span><span class="token punctuation">"</span>oatmeal-raisin<span class="token punctuation">"</span></span><span class="token punctuation">></span></span> <span class="token tag"><span class="token tag"><span class="token punctuation">&lt;</span>input</span> <span class="token attr-name">id</span><span class="token attr-value"><span class="token punctuation attr-equals">=</span><span class="token punctuation">"</span>oatmeal-raisin<span class="token punctuation">"</span></span> <span class="token attr-name">type</span><span class="token attr-value"><span class="token punctuation attr-equals">=</span><span class="token punctuation">"</span>checkbox<span class="token punctuation">"</span></span><span class="token punctuation">/></span></span> <span class="token tag"><span class="token tag"><span class="token punctuation">&lt;</span>div</span> <span class="token attr-name">class</span><span class="token attr-value"><span class="token punctuation attr-equals">=</span><span class="token punctuation">"</span>pretty-checkbox<span class="token punctuation">"</span></span><span class="token punctuation">></span></span><span class="token tag"><span class="token tag"><span class="token punctuation">&lt;/</span>div</span><span class="token punctuation">></span></span> Oatmeal <span class="token entity named-entity" title="&amp;">&amp;amp;</span> raisin <span class="token tag"><span class="token tag"><span class="token punctuation">&lt;/</span>label</span><span class="token punctuation">></span></span> <span class="token tag"><span class="token tag"><span class="token punctuation">&lt;/</span>fieldset</span><span class="token punctuation">></span></span></code></pre> <p>VoiceOver:</p> <blockquote> <p>Which of these cookies can we use?, group</p> </blockquote> <h3 id="buttons" tabindex="-1">Buttons</h3> <p>The &quot;buttons&quot; - actually <code>&lt;div&gt;</code> elements with added <code>onclick</code> attributes - are recognised as &quot;Clickable&quot; by Voiceover. Not all screenreader software is able to identify clickable elements in this way.</p> <blockquote> <p>Accept all, clickable</p> </blockquote> <p>However, if I'm navigating through the page using only the keyboard, I can't select one because the browser doesn't know it's a button.</p> <p>Sure, we <em>could</em> go adding a <code>tabindex</code> attribute to these buttons to make them tabbable, but then you run the risk of messing up the natural tabbing order of the document. The best solution is to <strong>use a <code>&lt;button&gt;</code> element</strong>. That's what it's for, and you can style it pretty much any way you like using CSS. You can even <a href="https://codepen.io/sophiekoonin/pen/oNZdebX">make a button look like a link, and vice versa</a>, which is something I do a lot when building a website to a design spec.</p> <pre class="language-css"><code class="language-css"> <span class="token selector">.button</span> <span class="token punctuation">{</span> <span class="token property">cursor</span><span class="token punctuation">:</span> pointer<span class="token punctuation">;</span> <span class="token property">display</span><span class="token punctuation">:</span> flex<span class="token punctuation">;</span> <span class="token property">border</span><span class="token punctuation">:</span> 0<span class="token punctuation">;</span> <span class="token property">box-shadow</span><span class="token punctuation">:</span> none<span class="token punctuation">;</span> <span class="token property">border-radius</span><span class="token punctuation">:</span> 6px<span class="token punctuation">;</span> <span class="token property">font-weight</span><span class="token punctuation">:</span> bold<span class="token punctuation">;</span> <span class="token property">font-size</span><span class="token punctuation">:</span> 1rem<span class="token punctuation">;</span> <span class="token property">margin</span><span class="token punctuation">:</span> auto 0<span class="token punctuation">;</span> <span class="token property">justify-content</span><span class="token punctuation">:</span> center<span class="token punctuation">;</span> <span class="token property">align-items</span><span class="token punctuation">:</span> center<span class="token punctuation">;</span> <span class="token property">padding</span><span class="token punctuation">:</span> 0.8rem 1rem<span class="token punctuation">;</span> <span class="token property">color</span><span class="token punctuation">:</span> white<span class="token punctuation">;</span> <span class="token property">background-color</span><span class="token punctuation">:</span> #420a55<span class="token punctuation">;</span> <span class="token punctuation">}</span> <span class="token selector">.button:hover</span> <span class="token punctuation">{</span> <span class="token property">background</span><span class="token punctuation">:</span> #8d6c99<span class="token punctuation">;</span> <span class="token punctuation">}</span></code></pre> <blockquote> <p>Accept all, button</p> <p>You are currently on a button... to click this button, press Ctrl+Option+Space.</p> </blockquote> <h3 id="headings" tabindex="-1">Headings</h3> <p>We've got three H1 elements on the page: the site name, the article headline, and the cookie banner. A site should only ever have one <code>&lt;h1&gt;</code> element - it's the main heading that tells you what the page is about. Any other headings should be nested underneath, or possibly not even headings at all (in the case of &quot;The Woofer Times&quot;, this doesn't need to be an actual heading).</p> <p>There's a subheading within the article which is simply bold text. That should be a lower-level nested heading, in this case an <code>&lt;h2&gt;</code>.</p> <p>Screenreaders allow users to navigate by headings, so it's important they're in a logical order.</p> <p>If you want big text, style it with CSS rather than relying on the browser default size of heading elements.</p> <h3 id="bold-and-italic-text" tabindex="-1">Bold and italic text</h3> <p>The byline and the image caption are displayed in italics using the <code>&lt;em&gt;</code> tag. However, screen readers may actually read this out differently because they expect the <code>&lt;em&gt;</code> tag to signal <b>emphasis</b> - that's what <code>em</code> means. So the tone of voice might change as it's being read out, with additional stress. The italics in the byline and caption are a stylistic choice, so we should italicise it with CSS rather than <code>&lt;em&gt;</code>.</p> <p>The <code>&lt;i&gt;</code> tag still exists alongside the <code>&lt;em&gt;</code> tag, but represents text that stands out from the rest of the body, such as scientific names, thoughts, technical terms and idiomatic terms from other languages:</p> <pre class="language-html"><code class="language-html">The red fox, <span class="token tag"><span class="token tag"><span class="token punctuation">&lt;</span>i</span><span class="token punctuation">></span></span>Vulpes vulpes<span class="token tag"><span class="token tag"><span class="token punctuation">&lt;/</span>i</span><span class="token punctuation">></span></span>, is commonly found... It was a case of <span class="token tag"><span class="token tag"><span class="token punctuation">&lt;</span>i</span> <span class="token attr-name">lang</span><span class="token attr-value"><span class="token punctuation attr-equals">=</span><span class="token punctuation">"</span>fr<span class="token punctuation">"</span></span><span class="token punctuation">></span></span>déjà vu<span class="token tag"><span class="token tag"><span class="token punctuation">&lt;/</span>i</span><span class="token punctuation">></span></span>. <span class="token tag"><span class="token tag"><span class="token punctuation">&lt;</span>i</span><span class="token punctuation">></span></span>Sure<span class="token tag"><span class="token tag"><span class="token punctuation">&lt;/</span>i</span><span class="token punctuation">></span></span>, she thought, <span class="token tag"><span class="token tag"><span class="token punctuation">&lt;</span>i</span><span class="token punctuation">></span></span>that must be it<span class="token tag"><span class="token tag"><span class="token punctuation">&lt;/</span>i</span><span class="token punctuation">></span></span>. I <span class="token tag"><span class="token tag"><span class="token punctuation">&lt;</span>em</span><span class="token punctuation">></span></span>really<span class="token tag"><span class="token tag"><span class="token punctuation">&lt;/</span>em</span><span class="token punctuation">></span></span> don't like this book. Do it <span class="token tag"><span class="token tag"><span class="token punctuation">&lt;</span>em</span><span class="token punctuation">></span></span>now<span class="token tag"><span class="token tag"><span class="token punctuation">&lt;/</span>em</span><span class="token punctuation">></span></span>! No, <span class="token tag"><span class="token tag"><span class="token punctuation">&lt;</span>em</span><span class="token punctuation">></span></span>I'm<span class="token tag"><span class="token tag"><span class="token punctuation">&lt;/</span>em</span><span class="token punctuation">></span></span> Spartacus.</code></pre> <p>The same goes for any bold text - <code>&lt;b&gt;</code> indicates significance or keywords and may also result in the screenreader voice changing when read out, so use CSS if the effect is purely presentational.</p> <p>Somewhat confusingly, both <code>&lt;strong&gt;</code> and <code>&lt;b&gt;</code> are used in slightly different situations despite both rendering as bold text in browsers - <code>&lt;strong&gt;</code> &quot;indicates that its contents have strong importance, seriousness, or urgency&quot; (<a href="https://developer.mozilla.org/en-US/docs/Web/HTML/Element/strong">MDN</a>), where as <code>&lt;b&gt;</code> is used to draw attention without the urgency (e.g. for relevant terms in a blog post like this one).</p> <pre class="language-html"><code class="language-html">I won't tell you again, <span class="token tag"><span class="token tag"><span class="token punctuation">&lt;</span>strong</span><span class="token punctuation">></span></span>do not disconnect the fire alarm<span class="token tag"><span class="token tag"><span class="token punctuation">&lt;/</span>strong</span><span class="token punctuation">></span></span>. This is known as <span class="token tag"><span class="token tag"><span class="token punctuation">&lt;</span>b</span><span class="token punctuation">></span></span>semantic HTML<span class="token tag"><span class="token tag"><span class="token punctuation">&lt;/</span>b</span><span class="token punctuation">></span></span>.</code></pre> <p>NB. Frustratingly, Markdown doesn't seem to distinguish between these elements, so everything is <code>&lt;strong&gt;</code> or <code>&lt;em&gt;</code>. If you want to use <code>&lt;b&gt;</code> or <code>&lt;i&gt;</code> you'll have to add them in as HTML within your Markdown.</p> <h3 id="image-caption" tabindex="-1">Image caption</h3> <p>It's clear to sighted users that the text under the image is a caption, but not to screen readers - there's nothing associating the two at all.</p> <p>We can fix that by wrapping the image in a <code>&lt;figure&gt;</code> element, then adding a <code>&lt;figcaption&gt;</code> underneath the image.</p> <pre class="language-html"><code class="language-html"> <span class="token tag"><span class="token tag"><span class="token punctuation">&lt;</span>figure</span> <span class="token attr-name">class</span><span class="token attr-value"><span class="token punctuation attr-equals">=</span><span class="token punctuation">"</span>image<span class="token punctuation">"</span></span><span class="token punctuation">></span></span> <span class="token tag"><span class="token tag"><span class="token punctuation">&lt;</span>img</span> <span class="token attr-name">alt</span><span class="token attr-value"><span class="token punctuation attr-equals">=</span><span class="token punctuation">"</span>a lovely fluffy samoyed<span class="token punctuation">"</span></span> <span class="token attr-name">src</span><span class="token attr-value"><span class="token punctuation attr-equals">=</span><span class="token punctuation">"</span>https://codepen-assets.s3.eu-west-2.amazonaws.com/alex-russell-saw-Dj8cxyi9ink-unsplash-1.jpg<span class="token punctuation">"</span></span> <span class="token punctuation">/></span></span> <span class="token tag"><span class="token tag"><span class="token punctuation">&lt;</span>figcaption</span> <span class="token attr-name">class</span><span class="token attr-value"><span class="token punctuation attr-equals">=</span><span class="token punctuation">"</span>caption<span class="token punctuation">"</span></span><span class="token punctuation">></span></span>"The most angery pupper I have ever seen" Photo: <span class="token tag"><span class="token tag"><span class="token punctuation">&lt;</span>a</span> <span class="token attr-name">href</span><span class="token attr-value"><span class="token punctuation attr-equals">=</span><span class="token punctuation">"</span>https://unsplash.com/@alexrussellsaw<span class="token punctuation">"</span></span><span class="token punctuation">></span></span>Alex Russell-Saw<span class="token tag"><span class="token tag"><span class="token punctuation">&lt;/</span>a</span><span class="token punctuation">></span></span> <span class="token tag"><span class="token tag"><span class="token punctuation">&lt;/</span>figcaption</span><span class="token punctuation">></span></span> <span class="token tag"><span class="token tag"><span class="token punctuation">&lt;/</span>figure</span><span class="token punctuation">></span></span></code></pre> <p>VoiceOver says:</p> <blockquote> <p>&quot;The most angery pupper I have ever seen&quot; Photo: Alex Russell-Saw, figure</p> </blockquote> <p>before then reading out the image information:</p> <blockquote> <p>a lovely fluffy samoyed, image</p> </blockquote> <h2 id="you-can-still-use-divs" tabindex="-1">You can still use <code>div</code>s!</h2> <p>As I demonstrated above, <code>&lt;div&gt;</code> elements can be extremely misused. That's not to say you shouldn't use them <em>at all</em>: on the contrary. But you should only use them for their intended purpose: entirely meaningless containers. I use them to group elements together which I then style with CSS. Or I'll wrap the entire page in a <code>&lt;div&gt;</code> and make it into a flex container for my <code>&lt;header&gt;</code>, <code>&lt;main&gt;</code> and <code>&lt;footer&gt;</code>. <code>&lt;div&gt;</code>s are great, as long as you don't use them in place of more meaningful elements.</p> <p>You can use the Dev Tools inspector on this very website and see the way I've used <code>&lt;div&gt;</code>s presentationally: for example, all the stars in the header are divs which are totally hidden from screenreaders, because they're entirely decorative.</p> <h2 id="the-same-news-site-with-semantic-tags" tabindex="-1">The same news site, with semantic tags</h2> <p>I've recreated the same site using semantic HTML tags, and it looks pretty much exactly the same (save some different margins here and there that are easy enough to fix).</p> <p class="codepen" data-height="265" data-theme-id="dark" data-default-tab="result" data-user="sophiekoonin" data-slug-hash="yLMjMgQ" data-preview="true" data-pen-title="Semantic HTML blog post example: semantic HTML"> <span>See the Pen <a href="https://codepen.io/sophiekoonin/pen/yLMjMgQ"> Semantic HTML blog post example: semantic HTML</a> by <a href="https://codepen.io/sophiekoonin">@sophiekoonin</a> on <a href="https://codepen.io">CodePen</a>.</span> </p> <script async="" src="https://cpwebassets.codepen.io/assets/embed/ei.js"></script> <p><strong>There is still a lot of room for improvement</strong>. ARIA attributes come into play here, to add extra information about the roles of each part of the page. I'm just illustrating the semantic elements in this example.</p> <p>Here are the main differences:</p> <ul> <li>the page is broken up into <code>&lt;header&gt;</code>, <code>&lt;main&gt;</code> and <code>&lt;footer&gt;</code>, with an <code>&lt;article&gt;</code> round the main page content</li> <li>there is only one <code>&lt;h1&gt;</code> on the page, because it signifies the main heading that tells you what the page is about</li> <li>the &quot;Accept all&quot; and &quot;Close&quot; buttons are actual <code>&lt;button&gt;</code> elements</li> <li>the checkboxes are <code>&lt;input type=&quot;checkbox&quot;&gt;</code>, with <code>&lt;label&gt;</code>s on each one <ul> <li>they're also inside a <code>&lt;fieldset&gt;</code> with a <code>&lt;legend&gt;</code> explaining what the checkboxes are for</li> </ul> </li> <li>the menu links are in an unordered list, <code>&lt;ul&gt;</code>, wrapped in a <code>&lt;nav&gt;</code></li> <li>the subheadings in the article are now <code>&lt;h2&gt;</code> (the first level of nesting under the main heading)</li> <li>the byline and caption are styled italic through CSS, rather than <code>&lt;em&gt;</code> tags</li> <li>I've used a <code>&lt;figure&gt;</code> for the image, with a <code>&lt;figcaption&gt;</code> for the caption to indicate it's a caption for that image</li> </ul> <h2 id="semantics-from-the-start-accessibility-from-the-start" tabindex="-1">Semantics from the start = accessibility from the start</h2> <p>Using semantic HTML as building blocks for a website will give you a lovely accessible foundation upon which to add your fancy CSS and whizzy JavaScript. You can use semantic elements in JS frameworks, too - I write React every day at work, and always use semantic elements. Your users (and/or your customers) will thank you for it.</p> <h2 id="find-out-more-about-semantic-html" tabindex="-1">Find out more about semantic HTML</h2> <ul> <li><a href="https://developer.mozilla.org/en-US/docs/Web/HTML/Element">MDN: HTML Elements reference</a> - with explanations of each element's purpose</li> <li><a href="https://learn-the-web.algonquindesign.ca/topics/html-semantics-cheat-sheet">Semantic HTML cheat sheet</a></li> </ul> A typical day: pandemic edition 2021-01-18T00:00:00Z https://localghost.dev/blog/a-typical-day-pandemic-edition/ <p><em>This is a series started by <a href="http://cdevroe.com/2021/01/07/my-typical-day/">Colin Devroe</a>. Some other lovely folks have written their own, including <a href="https://www.sarasoueidan.com/desk/typical-day/">Sara Soueidan</a> and <a href="https://www.cassie.codes/posts/my-typical-day/">Cassie Evans</a>.</em></p> <p>This post would have looked very different prior to March 2020. Before then, I'd be out at events, meetups or social meetings several nights a week, in the office every day seeing people, and commuting to and from work. Needless to say, as someone who gets energy from being around others, the change to working from home full-time and not going out has had a massive impact on my mental health and my depression has got much worse. I don't think I would have been nearly as <a href="https://localghost.dev/2020/12/give-yourself-a-break-lessons-from-burnout/">burnt out</a> as I was in December if all of this hadn't been going on. So this is my &quot;current normal&quot;.</p> <h2 id="my-typical-day" tabindex="-1">My typical day</h2> <p><strong>08:00 - 09:00:</strong> wake up, about an hour later than I used to. I'll usually sit in bed for a bit drinking coffee, talking to my husband, and checking the news/Twitter/notifications (it's a habit I'm trying to break, but not doing very well).</p> <p>At some point during this hour I'll scrape myself out of bed and shower, get dressed (I can't work in PJs!) and find some breakfast. If I'm honest, this is usually about 15 mins before I start work, because my bed is extremely comfortable and it's cold in the house.</p> <p><strong>09:00</strong>: catch up on Slack messages, see if there's anything that needs my attention, say hi to the team.</p> <p>We usually have a quick standup at 10ish, then I get stuck into whatever I'm working on. My days are often peppered with 1:1s, planning meetings and various other things - PR reviews, answering questions on Slack, triaging bugs. At the moment I find it hard to focus for long periods of time so whatever I'm doing I'll inevitably be distracted by notifications (I need to get into the habit of turning off Slack while I work).</p> <p>Depending on what I'm working on, I might be doing web work in React or backend work in Go, or I might be writing a proposal for a feature or technical change.</p> <p><strong>12:00 - 13:00</strong>: lunchtime. Quite an early lunch, a relic from when I used to be up at 7, eating breakfast at 7:30 and hungry by 12. I'll often go for a walk in the park and listen to a podcast (<a href="https://darknetdiaries.com/">Darknet Diaries</a>, <a href="https://switchedonpop.com/">Switched On Pop</a> and <a href="https://strongsongspodcast.com/">Strong Songs</a> are my favourites) or chat to my husband. On especially good days, we might go down to our garage where we have a little gym set up - I've been doing some free weights.</p> <p>I miss buying lunch, which is undisputably the most annoying meal to have to make.</p> <p><strong>13:00 - 18:00:</strong> the rest of my working day - more coding and 1:1s. I block out parts of the day in my calendar so that I have uninterrupted time with no meetings.</p> <p>At points I'll go downstairs to refill my drink and pester my husband.</p> <p><strong>18:00:</strong> I log off from work. I'm very strict about what time I finish work - never after 6pm.</p> <p>Once every couple of weeks I have a therapy session via video call, which has been massively helpful this year.</p> <p>I'm a creative person and I originally imagined I'd spend a lot of the free time I gained from not commuting doing things like making music or building cool things, but in reality I just browse the internet or play a video game.</p> <p>I've been thinking a lot about <a href="https://butyoudontlooksick.com/articles/written-by-christine/the-spoon-theory/">spoon theory</a> recently. Originally conceived as an analogy to living with chronic illness, I think it fits perfectly with the way my depression has been manifesting itself this year: I just haven't got enough spoons right now to do all the things I want to do. So often when I feel I should be being productive or doing something creative, all I'll have the energy to do is just sit down and play video games. Over Xmas it was Assassin's Creed: Valhalla, then a lot of <a href="https://www.stardewvalley.net/">Stardew Valley</a>, and I've racked up an embarrassing number of hours on <a href="https://www.twopointhospital.com/">Two Point Hospital</a> over the last few weeks.</p> <p><strong>19:00:</strong> time to make dinner, or continue to veg out while my husband makes dinner. Or we give up and order a takeaway.</p> <p>We've been making an extra effort to eat at the table over the last few months, which is nice because we actually talk to each other and pay attention to what we're eating instead of just zoning out in front of the TV 🙈</p> <p>Generally after dinner we'll watch TV, or continue playing video games next to each other because we're adults and make sensible choices. Sometimes I'll pick up whatever embroidery I have in progress and do it while we watch TV - I'm currently embroidering my wedding flowers.</p> <p><picture><source type="image/webp" srcset="https://localghost.dev/img/ufEkQdUFTl-280.webp 280w, https://localghost.dev/img/ufEkQdUFTl-640.webp 640w" sizes="auto"><img loading="lazy" decoding="async" src="https://localghost.dev/img/ufEkQdUFTl-280.jpeg" alt="" title="An embroidery in progress featuring statice, a peony, roses and a gerbera" width="640" height="675" srcset="https://localghost.dev/img/ufEkQdUFTl-280.jpeg 280w, https://localghost.dev/img/ufEkQdUFTl-640.jpeg 640w" sizes="auto"></picture><br> <picture><source type="image/webp" srcset="https://localghost.dev/img/o0oQBveAF2-280.webp 280w, https://localghost.dev/img/o0oQBveAF2-640.webp 640w, https://localghost.dev/img/o0oQBveAF2-960.webp 960w" sizes="auto"><img loading="lazy" decoding="async" src="https://localghost.dev/img/o0oQBveAF2-280.jpeg" alt="" title="A photo of my wedding bouquet" width="960" height="1200" srcset="https://localghost.dev/img/o0oQBveAF2-280.jpeg 280w, https://localghost.dev/img/o0oQBveAF2-640.jpeg 640w, https://localghost.dev/img/o0oQBveAF2-960.jpeg 960w" sizes="auto"></picture></p> <p>I've started keeping a little journal of good things that happen every day. They can be as minor as &quot;made a <a href="https://www.bbcgoodfood.com/recipes/aubergine-tomato-halloumi-pie">really nice pie</a>&quot; or something like &quot;got really positive feedback on the proposal I wrote&quot;. I tend to fixate on negative things, so having written evidence that good stuff is still happening should hopefully anchor me a bit and remind me of the positive side. I recommend it! Plus it's an excuse to buy a notebook and some nice pens.</p> <p><strong>23:00:</strong> time for bed. Over the last six years I've been going to bed at 11pm so religiously that I actually get sleepy at 11pm automatically. I try and read a bit before bed (currently making my way through Elena Ferrante's Neapolitan Novels) but usually I only manage a few pages before I conk out.</p> <p>I feel like this wasn't a particularly exciting or enlightening typical day, but really, how can it be? Like <a href="https://www.cassie.codes/posts/my-typical-day/">Cassie</a> says: &quot;If you're reading these posts - whatever your day looks like - whether you smashed through a huge to-do list or not. You're doing good, remember to be kind to yourself.&quot;.</p> <p>All we can really do is make it through the day, focus on the tiny things that make us happy, and know that there is a light at the end of the tunnel. At the time of writing, 5.8% of the UK population have received their first dose of the vaccine (including my dad, hey dad!). Good news is out there, it's just not as clickbaity as the bad news, so we see less of it.</p> <p>Now, if you'll excuse me, I have a fake hospital to run.</p> Give yourself a break: lessons from burnout 2020-12-16T00:00:00Z https://localghost.dev/blog/give-yourself-a-break-lessons-from-burnout/ <p>I started writing this post a few days ago, and was so exhausted I couldn't actually be bothered to finish it, which tells you a lot really. And if you're too exhausted to read another blog post, here's a summary: have a rest. Go and do something nice. Tech can wait.</p> <p>A couple of weeks ago (possibly - I have no concept of time any more) I tweeted this, which seemed to resonate with folks:</p> <blockquote> <p>the only coding I've done outside of work in quite a long time is a small bit of JS to apply hats to emoji. I don't know how anyone does tech stuff out of hours, it's exhausting enough doing it IN hours</p> </blockquote> <p>(A plug for <a href="https://emojinator.fun">emojinator</a>, the site in question where you can add hats to emoji)</p> <p>Twitter is awash with fantastically talented people building incredible stuff and sharing it, as well as those insufferable grifters who tweet things like &quot;15 things you must learn to be a good developer #100daysofcode&quot; and then hawk you their ebook. &quot;Thought leaders&quot; (don't you just hate that term) are churning out article after article of brilliant leadership advice, technical deep-dives, creative things to do in CSS, you name it. I don't know about you, but the article count on my RSS reader has been increasing for months and I just have <em>not</em> been reading anything. I can barely focus at the moment.</p> <p>I became a tech lead in September, which is something I've been wanting to do for quite a while. One day I'll write about what I've learned as a new tech lead, but now is not the time, because I don't think I've been able to really succeed in my role due to the constant being-on-fire-ness of this entire year. Instead, have some lessons from the trash fire of 2020 that we can take over into the slightly-smaller-but-still-burning trash fire of 2021.</p> <h2 id="lesson-number-1-give-yourself-a-break" tabindex="-1">Lesson number 1: give yourself a break</h2> <p>As a new tech lead, I've felt like I'm letting my team down because I don't have the mental capacity to sit and read about the things I need to improve on, like observability and monitoring. I've had a copy of the SRE Workbook on my laptop waiting to be read, which I've barely touched.</p> <p>Where did all these expectations come from? Myself, mostly. Nobody is peering over my shoulder going &quot;why aren't you reading about Prometheus?&quot;. (Actually, my team have been nothing but supportive this entire year, and I love them all.)</p> <p>The coronavirus pandemic has meant that my <a href="https://shechoir.com/london">choir</a> hasn't been able to meet since March. The choir I've been a part of for seven years, have been co-running for five of those years, and which forms a huge part of my own identity. I'm an extrovert who gets energy from being around people (but not in a Colin Robinson kind of way). Most of us have barely seen our friends and family. This year has been intensely difficult. I'm sure you've all experienced something similar.</p> <p>On top of that, the political situation is dire. As well as the absolute corrupt shambles of a government who have done such a terrible job of handling the COVID pandemic that we have the highest death rate in Europe, there's a big red countdown timer on the UK government website that counts down to Brexit when I'm looking up just what the hell being in Tier 3 actually means. Many, many people have lost loved ones, there's a whole wave of anti-vaccine &quot;Keep Britain Free&quot; zealots popping up all over the place, and the UK has been nicknamed &quot;TERF island&quot; because of the rampant transphobia being spouted by formerly respectable public figures. And that's just on our doorsteps in the UK.</p> <p>The SRE Workbook can wait, basically. We have enough on our collective plates.</p> <h2 id="lesson-number-2-if-it-benefits-your-work-it-can-be-done-in-working-hours" tabindex="-1">Lesson number 2: if it benefits your work, it can be done in working hours</h2> <p>Perhaps you used to watch conference talks on the daily commute, or read technical books in bed. I definitely had a copy of The Manager's Path next to the bed that I was chipping away at at one point. But now that commute is gone for many of us, and I don't know about you but the only thing I have the energy for in the evenings is playing video games or idly browsing the internet.</p> <p>I believe quite strongly that if you are learning something in order to become better at your job, you should be doing that within working hours. In my team, we all know that <em>technically</em> it's encouraged to take time out to learn something - we get a budget to do just that - but we rarely ever do it. To that end, I actually scheduled 2 hours a week in the whole team's calendar on a Friday for &quot;Reading time&quot;. They can use that time however they want: they can watch videos, try something out, read a book. They can take more time, or less time. They can do it on a different day, or skip it entirely. All it is is the explicit permission to take some time out of your week to learn something, <em>if you want</em>.</p> <p>(I don't know if they actually take the time: I haven't asked. I just want them to know they can, in writing.)</p> <h2 id="lesson-number-3-some-things-are-boring-and-that-s-okay" tabindex="-1">Lesson number 3: some things are boring and that's okay</h2> <p>Kubernetes does not interest me. I have the utmost respect for the infra folks on Twitter who seem super intelligent and knowledgeable about it, but I cannot bring myself to be interested in it. For a while I felt like not being into this stuff made me a lesser developer somehow. But my specialism lies elsewhere and there are things I'm good at that people who do Kubernetes for a living aren't (and I have it on good authority that &quot;do Kubernetes&quot; is a technical term). That's how well-functioning teams work.</p> <p>I know enough <code>kubectl</code> to get a list of pods and SSH into one of them (but mainly because I have the zsh autocomplete plugin and it fills it in for me based on my past commands). That's all I need: most of the actual Kubernetes wizardry is automated or managed by people who specialise in it (in my case, our excellent Infrastructure Platform team). A good developer or tech lead will know when it's the right time to delegate to someone who knows more about it than they do.</p> <h2 id="lesson-number-4-mental-health-is-more-important-than-kp-is" tabindex="-1">Lesson number 4: mental health is more important than KPIs</h2> <p>I'll caveat this section with the fact that I know many people work at companies where mental health is not something to be discussed, or something that people really seem to take into account. And I also know it's not as simple as &quot;find another job&quot; and that should never be the only solution.</p> <p>Instead, this is an appeal to senior leadership and management. Your employees are not inanimate batteries you plug in to power your company. They are humans, with complicated lives, families they haven't been able to see for months, and children having to stay home from school because someone in their class tested positive for COVID. Their partner might have lost their job this year. They may be struggling with working from home, missing being around people in the office, or they might be finding video calls even more exhausting than real-life meetings. If you've sadly had to make redundancies this year, are you expecting the reduced number of people to do the same amount of work as before?</p> <p>If you're a team lead, check in on your team. Are they working long hours? Encourage them to sign off earlier. I highly recommend this <a href="https://twitter.com/kkukshtel/status/1338240765605109762">Twitter thread</a> about why working late a couple of hours a week can lead to completely unrealistic expectations of how long projects take to complete.</p> <p>Schedule 1:1s with them and ask them how they're feeling about things. Encourage them to share their workload if they have too much: having more on your plate than you can cope with right now is not a sign of failure.</p> <p>Mental health should be taken as seriously as physical health. We have trained <a href="https://mhfaengland.org/">mental health first aiders</a> who can provide a swift response over Slack, an Employee Assistance Program for access to counselling and legal information, and people can take mental health days if they are struggling. I've taken three afternoons off at the last minute in the past three weeks because I was so exhausted with <em>[gestures wildly]</em> everything, and my team were very understanding about it. I know I am privileged in that regard, and I am grateful.</p> <p>Ultimately, the productivity and success of the company will suffer more if your team burn out. People will start taking extended leave, or they'll quit. It's a choice between delaying a product release by a day and the mental health of a human being.</p> <p>--</p> <p>Sorry folks, I wrote a long article when I promised you you didn't have to read articles. Go and have a cup of tea and look out of the window for a bit.</p> <blockquote> <p>I guess what I'm saying is I'm really tired, it's okay if you're really tired, let's all be really tired and accept that the Google SRE Workbook will stay untouched on the shelf for a few more weeks</p> </blockquote> 7 myths designers and developers believe about web accessibility 2020-10-11T00:00:00Z https://localghost.dev/blog/7-myths-designers-and-developers-believe-about-web-accessibility/ <p>In an ideal world, being &quot;good at accessibility&quot; wouldn't make you stand out from the crowd. Companies wouldn't be hiring accessibility experts to help them unpick and untangle the inaccessible products they've been building for years. Speaking about web accessibility at a conference would be as unnecessary as getting up on stage and giving a talk on how to write HTML.</p> <p>But we don't live in that world, and the web is full of inaccessible websites. Try navigating the <a href="https://libertylondon.com">Liberty London</a> website with just your keyboard - can you add something to your basket?</p> <p>I want to bust a few myths that I've come across over the years, in the hope that accessibility will stop being something to &quot;deal with later&quot; and start being something that people think about from the start.</p> <h2 id="myth-1-the-majority-of-your-users-don-t-have-access-needs" tabindex="-1">Myth 1: The majority of your users don't have access needs</h2> <p>Repeat after me: there's no such thing as a &quot;typical&quot; or &quot;normal&quot; user.</p> <p>When we're designing and building websites or apps, it's all too easy to forget that we are coming from a narrow perspective. My own abilities, experiences and knowledge shape how I interact with a website, and my experience may be totally different from yours. So even if you know instinctively that a particular icon means &quot;download&quot;, others might not make that connection.</p> <figure><picture><source type="image/webp" srcset="https://localghost.dev/img/AJFEj4YwX4-280.webp 280w, https://localghost.dev/img/AJFEj4YwX4-640.webp 640w" sizes="auto"><img loading="lazy" decoding="async" src="https://localghost.dev/img/AJFEj4YwX4-280.jpeg" alt="Spiderman pointing at himself" width="640" height="305" srcset="https://localghost.dev/img/AJFEj4YwX4-280.jpeg 280w, https://localghost.dev/img/AJFEj4YwX4-640.jpeg 640w" sizes="auto"></picture><figcaption>Is this how you see your users?</figcaption></figure> <p>Microsoft's brilliant <a href="https://www.microsoft.com/design/inclusive/">Inclusive Design Toolkit</a> warns of the consequences of this selective thinking:</p> <blockquote> <p>“If we use our own abilities and biases as a starting point, we end up with products designed for people of a specific gender, age, language ability, tech literacy, and physical ability. Those with specific access to money, time, and a social network.”</p> </blockquote> <p>As a non-disabled, computer-literate developer with a Macbook and fibre internet, I need to make sure I'm not only building apps for people like me. All my users will have different requirements, different technology and different backgrounds. As the Inclusive Design Toolkit puts it:</p> <blockquote> <p>“When it comes to people, there’s no such thing as “normal.” The interactions we design with technology depend heavily on what we can see, hear, say, and touch. Assuming all those senses and abilities are fully enabled all the time creates the potential to ignore much of the range of humanity.&quot;</p> </blockquote> <h2 id="myth-2-accessibility-is-optional" tabindex="-1">Myth 2: Accessibility is optional</h2> <p>It's actually the law, in both the UK and the USA (I can't speak for other countries, but I'd be keen to hear from anyone who knows about any legislation in other places).</p> <p>In the UK, The Equality Act 2010 makes it illegal to discriminate against people with disabilities - and that includes by accident! It doesn’t explicitly refer to websites, but the Equality and Human Rights Commission’s (EHRC) Code of Practice does list websites as one of the services to the public that should be considered covered by the Equality Act.</p> <p>According to the EHRC, an organisation &quot;is responsible for ensuring that reasonable adjustments have been made where needed, for example by changing the size of the font, to ensure that disabled users are able to get the information, without being placed at a substantial disadvantage (even if the [organization] employs an external organisation to build and maintain its website).&quot;</p> <p>The EHRC may:</p> <ul> <li>Conduct formal investigations</li> <li>Serve non-discrimination notices</li> <li>Act over persistent discrimination</li> <li>Issue <a href="https://www.equalityhumanrights.com/sites/default/files/employercode.pdf">Codes of Practice</a></li> <li>Help someone prosecute a company - this requires someone to actually sue the company, which there's no case law for at the moment - but that could change any time.</li> </ul> <p>In the US, there's the Americans with Disabilities Act (ADA). Famously, <a href="https://www.bbc.co.uk/news/technology-46894463">Domino's Pizza was sued</a> by a blind person who couldn't use the website with a screenreader. According to US law firm <a href="https://www.adatitleiii.com/2020/04/the-curve-has-flattened-for-federal-website-accessibility-lawsuits/">Seyfarth</a>, in 2019 there were 2,256 web accessibility lawsuits filed.</p> <h2 id="myth-3-access-needs-come-from-permanent-disabilities" tabindex="-1">Myth 3: Access needs come from permanent disabilities</h2> <p>The truth is, anyone at any time can have access needs, and they can be permanent, temporary or situational. Temporary impairments might be due to a medical condition, and situational impairments may result from the environment around us - a situation we're in.</p> <p>Some examples of permanent conditions:</p> <ul> <li>partial or full blindness</li> <li>having a limb amputated</li> <li>learning difficulties, such as dyslexia</li> </ul> <p>A temporary impairment could be:</p> <ul> <li>visual aura from a migraine</li> <li>repetitive strain injury making it difficult to use a mouse</li> <li>cognitive processing difficulties (also known as brain fog) following an illness</li> </ul> <p>Situational impairments might include:</p> <ul> <li>a bright light causing glare on the screen</li> <li>loud noise, making it difficult to concentrate</li> <li>slow internet</li> </ul> <p>Even if you consider yourself not to have any form of disability, you could find yourself with a temporary or situational impairment at any point.</p> <p>Often when developers <em>do</em> think about accessibilty, they think of screen readers and people with visual impairments. That's a start, but there’s a spectrum of what constitutes disability. The important thing to note is that it doesn’t necessarily translate to some kind of personal health condition.</p> <p>According to the World Health Organisation:</p> <blockquote> <p>“Disability is not just a health problem. It is a complex phenomenon, reflecting the interaction between features of a person’s body and features of the society in which [they live].&quot;</p> </blockquote> <p>So much of the world around us can influence whether or not we have, or consider ourselves to have, a disability. Summed up perfectly by <a href="https://amandaleduc.com/">Amanda Leduc</a> (originally on Twitter):</p> <blockquote> <p>If you wear corrective lenses of any kind, you have a disability. And guess what: your disability has already been accommodated, which is part of why you might not see yourself as disabled.<br> @AmandaLeduc, Aug 30, 2020.</p> </blockquote> <h2 id="myth-4-accessibility-is-a-barrier-to-good-design" tabindex="-1">Myth 4: Accessibility is a barrier to good design</h2> <p>I've heard this one a lot. And as a web developer I feel like I’m often turning around to designers and saying “Sorry, I can’t do that, because it’s not accessible”. (The best designers will then turn around and say, &quot;okay, let's find something that works better.&quot;)</p> <p>It’s easy to imagine that in order for design to be truly accessible you have to build sites that look like the <a href="http://info.cern.ch/hypertext/WWW/TheProject.html">first-ever web page</a>, but that’s not the case.</p> <figure><picture><source type="image/webp" srcset="https://localghost.dev/img/g3UmiZHRy9-280.webp 280w, https://localghost.dev/img/g3UmiZHRy9-640.webp 640w, https://localghost.dev/img/g3UmiZHRy9-960.webp 960w" sizes="auto"><img loading="lazy" decoding="async" src="https://localghost.dev/img/g3UmiZHRy9-280.jpeg" alt="A screenshot of the first ever web page on cern.ch: white background, black text, blue hyperlinks, no styling whatsoever." width="960" height="503" srcset="https://localghost.dev/img/g3UmiZHRy9-280.jpeg 280w, https://localghost.dev/img/g3UmiZHRy9-640.jpeg 640w, https://localghost.dev/img/g3UmiZHRy9-960.jpeg 960w" sizes="auto"></picture><figcaption>Is this... accessible design?</figcaption></figure> <p>The reality is that accessible design <em>is</em> good design - and vice versa.</p> <p>When it comes down to it, can we really call a design good if it doesn’t work for everyone? You can build the sleekest, prettiest user interface, but if someone who uses the keyboard to get around the internet can’t see what they’re doing, are we really going to call that good design?</p> <p>As Jessie Hausler, the Director of Product Accessibility at Salesforce, wrote in his article <em><a href="https://medium.com/salesforce-ux/7-things-every-designer-needs-to-know-about-accessibility-64f105f0881b">7 Things Every Designer Needs to Know about Accessibility</a></em>:</p> <blockquote> <p>“Accessibility will not force you to make a product that is ugly, boring, or cluttered. It will introduce a set of constraints to incorporate as you consider your design.”</p> </blockquote> <p>If we can work within the constraints of the technology we're building for, or the constraints of what's in style, we can work within the constraints of accessible design.</p> <h2 id="myth-5-accessibility-is-hard-to-implement" tabindex="-1">Myth 5: Accessibility is hard to implement</h2> <p>This one has a grain of truth in: accessibility can be difficult to implement <em>retrospectively</em>. If you've built an entire web app without considering accessibility, it can take a lot of time (and cost money) to go back and fix it up so that it's accessible.</p> <p>But if you consider accessibility from the <em>start</em>, factoring it into your designs and the way you write your code, it doesn't have to be difficult. Accessibility by default is a lot easier than accessibility after the fact.</p> <p>As web developers, by sticking to some best practices in HTML we're already halfway there:</p> <ul> <li>using semantic HTML elements such as <code>&lt;article&gt;</code> and <code>&lt;nav&gt;</code> to mark up the different parts of the document (I'll write more about this soon!)</li> <li>using <code>&lt;button&gt;</code> for buttons with <code>onclick</code> attributes, and <code>&lt;a&gt;</code> for links to different pages (and not adding <code>onclick</code> attributes to anything except buttons!)</li> <li>nesting headings correctly, from <code>h1</code> through to <code>h6</code> , with only one <code>h1</code> on the page</li> <li>keeping HTML tags for markup, and using CSS for style (rather than, say, using a <code>&lt;h1&gt;</code> tag because you want big text)</li> </ul> <p>Many of the things that will help some groups of users with access needs will also benefit others: for example, good semantic HTML is great for screenreader users, but also helpful for keyboard or <a href="https://gettecla.com/blogs/news/introduction-to-assistive-switches">adaptive switch</a> users, as with the right tags the browser knows what should be focusable and what shouldn't. Labels on forms make them easier to read and understand for people of all cognitive abilities, and they are good for screenreader users too.</p> <p>Add an accessibility checklist to your JIRA tickets or pull request templates: make it part of your definition of done. Look out for best practices in your code reviews.</p> <h2 id="myth-6-react-apps-are-inherently-inaccessible" tabindex="-1">Myth 6: React apps are inherently inaccessible</h2> <p>I seem to follow two camps of people on Twitter: people in the React community, and people who hate the React community. I am by no means a hardcore React stan (and I think the community can be as toxic as any popular programming language/framework community can be) but I use it every day at work and I think it's just as good as some of the alternatives out there.</p> <p>One of the arguments against React that I hear most frequently is that React produces inaccessible websites which are <code>&lt;div&gt;</code>s all the way down. <strong>It doesn't.</strong> If React apps are a mess of un-semantic <code>&lt;div&gt;</code>s, it's because the developer put them there. The issue is not with the framework, but the lack of semantic HTML knowledge of the developers who use it: you can write any HTML you like in JSX.</p> <p>Ultimately, screen readers tend to ignore <code>&lt;div&gt;</code> components, as they're generic containers. If we use semantic HTML elements for the actual content of the page, there shouldn't be a problem at all from a markup perspective.</p> <p>React components can't return more than one element, so with older versions of React you had to wrap multiple elements in a container <code>&lt;div&gt;</code>. This would have added a few extra containers to the page. But the introduction of <a href="https://reactjs.org/docs/fragments.html">React Fragments</a> solved that, and usually doesn't result in any additional elements being added to the DOM.</p> <p>Another thing to watch out for is screen readers not alerting users when content on the page changes. This isn't a React problem specifically but a <a href="https://codeburst.io/building-accessible-single-page-apps-2ea3e4fbbc01">single-page application (SPA) problem</a>, and it can be solved with ARIA live regions. You'd still have this problem if you built your app in vanilla JavaScript.</p> <p>Ideally, your SPA should also work with JavaScript turned off - with React we have tools like <a href="https://localghost.dev/blog/7-myths-designers-and-developers-believe-about-web-accessibility/nextjs.org/">Next.js</a> which produce isomorphic (server- and client-side) applications.</p> <h2 id="myth-7-automated-testing-will-catch-all-accessibility-problems" tabindex="-1">Myth 7: Automated testing will catch all accessibility problems</h2> <p>People love to show off their Lighthouse scores, don't they?</p> <figure><picture><source type="image/webp" srcset="https://localghost.dev/img/iMgPSPeT_9-280.webp 280w, https://localghost.dev/img/iMgPSPeT_9-640.webp 640w, https://localghost.dev/img/iMgPSPeT_9-960.webp 960w" sizes="auto"><img loading="lazy" decoding="async" src="https://localghost.dev/img/iMgPSPeT_9-280.jpeg" alt="A screenshot of a github repo that offers Lighthouse badges for your repository or website so you can brag about your site's awesome Lighthouse performance" width="960" height="453" srcset="https://localghost.dev/img/iMgPSPeT_9-280.jpeg 280w, https://localghost.dev/img/iMgPSPeT_9-640.jpeg 640w, https://localghost.dev/img/iMgPSPeT_9-960.jpeg 960w" sizes="auto"></picture><figcaption>Is it though?</figcaption></figure> <p><a href="https://developers.google.com/web/tools/lighthouse/">Lighthouse</a> is an open-source automated testing tool that you can run as a Node module, from the CLI, from Chrome or as part of your CI checks. It checks for performance, accessibility, progressive web app performance, best practices and SEO. It's a great tool, but unfortunately it's often treated as a silver bullet. A high Lighthouse score is a good thing, but it doesn't mean that you've &quot;done&quot; accessibility - there are always things that Lighthouse won't be able to detect.</p> <p>The best way to test accessibility is to approach it from all sides. Automated tooling such as Lighthouse or <a href="https://www.deque.com/axe/">axe</a>) can and should be part of your development process, as it'll provide a quick feedback loop for common accessibility problems in your markup and CSS. But what these tools don't account for are things like the quirks of different screenreader software (and boy, do they have quirks), or the other kinds of assistive technology people will use to access your website. Lighthouse can't tell you whether your site is still legible when it's zoomed in 600%, or whether you can interact with the various parts of the app with a keyboard as you would with a mouse. The Accessibility in Government blog has a great article about <a href="https://accessibility.blog.gov.uk/2017/02/24/what-we-found-when-we-tested-tools-on-the-worlds-least-accessible-webpage/">what they found when they tested automated tooling on the world's least accessible webpage</a>.</p> <p>Before you merge your PR, check how your new feature behaves with screenreaders: Macs and iPhones come with <a href="https://www.apple.com/uk/accessibility/iphone/vision/">VoiceOver</a>, Android has <a href="https://support.google.com/accessibility/android/answer/6283677?hl=en">TalkBack</a>, Linux has <a href="https://help.gnome.org/users/orca/stable/introduction.html.en">Orca</a>, and you can download the open-source <a href="https://www.nvaccess.org/download/">NVDA</a> screenreader for Windows. Does it read out everything it's supposed to? Is it reading out anything it's <em>not</em> supposed to? Are the headings in the right order?</p> <p>Ultimately, there’s no substitute for getting people with actual access needs to use the website or app. You can do some user testing, commission a formal audit, and/or make sure you have ways for users to give you feedback. Include disabled users in your UX research upfront, as well, so that you're building something accessible right from the start.</p> <h2 id="it-s-your-turn" tabindex="-1">It's your turn</h2> <p>If you encounter any of these myths, <strong>you can (and should) challenge them</strong>. A lot of the time, these myths stick around because accessibility just isn't being talked about. Be the one to bring it up, and get others on board too. Bake accessibility into your design systems and your ways of working.</p> <h2 id="resources-and-further-reading" tabindex="-1">Resources &amp; further reading</h2> <ul> <li><a href="https://www.microsoft.com/design/inclusive/">Microsoft Inclusive Design Toolkit</a></li> <li><a href="https://medium.com/salesforce-ux/7-things-every-designer-needs-to-know-about-accessibility-64f105f0881b">7 Things Every Designer Needs to Know about Accessibility - Jessie Hausler</a></li> <li><a href="https://www.deque.com/accessible-design/">Deque - Accessible Design</a></li> <li><a href="https://www.deque.com/accessible-development/">Deque - Accessible Development</a></li> <li><a href="https://marcysutton.com/">Marcy Sutton</a> - blog posts and talks from an accessibility expert</li> <li><a href="https://accessibility.blog.gov.uk/2017/02/24/what-we-found-when-we-tested-tools-on-the-worlds-least-accessible-webpage/">What we found when we tested tools on the world's least accessible webpage</a> - Accessibility in Government Blog</li> </ul> Defending yourself against cross-site scripting attacks with Content-Security-Policy 2020-05-03T00:00:00Z https://localghost.dev/blog/defending-yourself-against-cross-site-scripting-attacks-with-content-security-policy/ <p>I spent an entire day last week wrestling with a PDF-rendering library in React which was refusing to work in production. Locally it ran just fine, but as soon as we built our app in production mode, it wasn't doing anything. Looking at the console, the errors it was spitting out made my heart sink. I'd seen these before.</p> <figure><a href="https://localghost.dev/img/blog/csp/eval-error.png"><picture><source type="image/webp" srcset="https://localghost.dev/img/PhbkP30qEH-280.webp 280w, https://localghost.dev/img/PhbkP30qEH-640.webp 640w" sizes="auto"><img loading="lazy" decoding="async" src="https://localghost.dev/img/PhbkP30qEH-280.jpeg" alt="Console error: Refused to evaluate a string as JavaScript because 'unsafe-eval' is not an allowed source of script in the following Content Security Policy directive: script-src 'self'." width="640" height="30" srcset="https://localghost.dev/img/PhbkP30qEH-280.jpeg 280w, https://localghost.dev/img/PhbkP30qEH-640.jpeg 640w" sizes="auto"></picture></a><figcaption>The dreaded console error</figcaption></figure> <p>This was an error caused by our <code>Content-Security-Policy</code> (CSP) header, telling our browser that something in the library should be blocked - specifically, an <code>eval()</code> function within one of the dependencies of our PDF-rendering library. I did what any self-respecting developer would do and complained at anyone who would listen - okay, I posted on Twitter - and then discovered that a lot of other developers weren't aware of what CSP was or what it was for. So I thought I'd write a post about it.</p> <p>The error messages I was getting were protecting me against a common web security vulnerability - <strong>cross-site scripting attacks</strong> (XSS). As a web developer, it's really important to be aware of what XSS is and how to prevent it.</p> <h2 id="contents" tabindex="-1">Contents <!-- omit in toc --></h2> <ul> <li><a href="https://localghost.dev/blog/defending-yourself-against-cross-site-scripting-attacks-with-content-security-policy/#cross-site-scripting-xss">Cross-site scripting (XSS)</a> <ul> <li><a href="https://localghost.dev/blog/defending-yourself-against-cross-site-scripting-attacks-with-content-security-policy/#stored-xss">Stored XSS</a></li> <li><a href="https://localghost.dev/blog/defending-yourself-against-cross-site-scripting-attacks-with-content-security-policy/#reflected-xss">Reflected XSS</a></li> <li><a href="https://localghost.dev/blog/defending-yourself-against-cross-site-scripting-attacks-with-content-security-policy/#dom-based-xss">DOM-based XSS</a></li> <li><a href="https://localghost.dev/blog/defending-yourself-against-cross-site-scripting-attacks-with-content-security-policy/#preventing-xss">Preventing XSS</a></li> </ul> </li> <li><a href="https://localghost.dev/blog/defending-yourself-against-cross-site-scripting-attacks-with-content-security-policy/#content-security-policy-is-a-set-of-rules-about-permitted-content-on-a-website">Content-Security-Policy is a set of rules about permitted content on a website</a> <ul> <li><a href="https://localghost.dev/blog/defending-yourself-against-cross-site-scripting-attacks-with-content-security-policy/#fetch-directives">Fetch directives</a></li> <li><a href="https://localghost.dev/blog/defending-yourself-against-cross-site-scripting-attacks-with-content-security-policy/#document-directives">Document directives</a></li> <li><a href="https://localghost.dev/blog/defending-yourself-against-cross-site-scripting-attacks-with-content-security-policy/#navigation-directives">Navigation directives</a></li> <li><a href="https://localghost.dev/blog/defending-yourself-against-cross-site-scripting-attacks-with-content-security-policy/#sources">Sources</a></li> <li><a href="https://localghost.dev/blog/defending-yourself-against-cross-site-scripting-attacks-with-content-security-policy/#unsafe-inline-and-the-risks-of-inline-styles"><code>unsafe-inline</code> and the risks of inline styles</a></li> <li><a href="https://localghost.dev/blog/defending-yourself-against-cross-site-scripting-attacks-with-content-security-policy/#scripts-unsafe-inline-and-unsafe-eval-why-eval-is-evil">Scripts, <code>unsafe-inline</code> and <code>unsafe-eval</code>: why <code>eval()</code> is evil</a></li> </ul> </li> <li><a href="https://localghost.dev/blog/defending-yourself-against-cross-site-scripting-attacks-with-content-security-policy/#how-to-create-a-csp-header">How to create a CSP header</a></li> <li><a href="https://localghost.dev/blog/defending-yourself-against-cross-site-scripting-attacks-with-content-security-policy/#adding-to-a-csp-header">Adding to a CSP header</a> <ul> <li><a href="https://localghost.dev/blog/defending-yourself-against-cross-site-scripting-attacks-with-content-security-policy/#styled-components-and-unsafe-inline">styled-components and <code>unsafe-inline</code></a></li> <li><a href="https://localghost.dev/blog/defending-yourself-against-cross-site-scripting-attacks-with-content-security-policy/#environment-based-csp">Environment-based CSP</a></li> </ul> </li> <li><a href="https://localghost.dev/blog/defending-yourself-against-cross-site-scripting-attacks-with-content-security-policy/#the-moral-of-the-story">The moral of the story</a></li> <li><a href="https://localghost.dev/blog/defending-yourself-against-cross-site-scripting-attacks-with-content-security-policy/#references--further-reading">References &amp; further reading</a></li> </ul> <h2 id="cross-site-scripting-xss" tabindex="-1">Cross-site scripting (XSS)</h2> <p>XSS involves someone injecting malicious code into an unsuspecting website, which then executes on the victim's computer.</p> <p>This injected code can do things like:</p> <ul> <li>copy your cookies and send them to the attacker</li> <li>get information about your location or data from your webcam etc</li> <li>grab session tokens from local storage</li> </ul> <p>On websites that store sensitive information, such as banking or shopping websites, XSS vulnerabilities could allow someone to impersonate you by stealing your access token and using it to log in as you - which means gold-plated toilet seats for them, and a nice credit card bill for you.</p> <p>The Open Web Application Security Project (OWASP) recognises three different methods of XSS: <strong>stored</strong>, <strong>reflected</strong> and <strong>DOM-based</strong>.</p> <h3 id="stored-xss" tabindex="-1">Stored XSS</h3> <p>Stored XSS refers to malicious code sent in the server response from something like a database.</p> <p><strong>Example</strong>: on a website allowing people to submit comments to discuss news articles, a user submits the following comment:</p> <pre><code>Great stuff guys!&lt;script&gt;/* sneaky code */&lt;/script&gt; </code></pre> <p>If the website isn't sanitizing any user input (stripping it of any HTML tags) and is rendering the comment directly into the DOM, that script will execute as soon as you load the page it's on. Before you know it, your browser has posted comments all over the site declaring your support for some deeply unpleasant organisation.</p> <p>There's a really interesting <a href="https://darknetdiaries.com/episode/61/">episode of the podcast Darknet Diaries</a> featuring Samy, the (accidental) creator of a MySpace worm that used XSS to get people's profiles to automatically add him as a friend. This is a great example of stored XSS, because it all started with something he posted on his own profile (which was then stored in MySpace's database).</p> <h3 id="reflected-xss" tabindex="-1">Reflected XSS</h3> <p>This is where malicious user input in a request is sent back in the immediate server response, executing on the receiving client's browser.</p> <p><strong>Example</strong>: someone posts a link online to a shopping website, with a search term in the URL so it takes you directly to the catalog page. But they've also included some <code>&lt;script&gt;</code> tags in the query string.</p> <p><code>https://niche-tshirts.com/shirts?q=extreme+ironing&lt;script&gt;/* sneaky code */&lt;/script&gt;</code></p> <p>When the server receives the request, it takes the entire query string and uses that as the search parameter. In the HTML that it returns, it renders:</p> <pre class="language-html"><code class="language-html"><span class="token tag"><span class="token tag"><span class="token punctuation">&lt;</span>p</span><span class="token punctuation">></span></span>You searched for: amateur paleontology<span class="token tag"><span class="token tag"><span class="token punctuation">&lt;</span>script</span><span class="token punctuation">></span></span><span class="token script"><span class="token language-javascript"><span class="token comment">/* sneaky code */</span></span></span><span class="token tag"><span class="token tag"><span class="token punctuation">&lt;/</span>script</span><span class="token punctuation">></span></span><span class="token tag"><span class="token tag"><span class="token punctuation">&lt;/</span>p</span><span class="token punctuation">></span></span></code></pre> <p>Then, your poor unsuspecting browser will execute what's between those script tags, and the attacker will be able to gain access to your access token and anything else they've managed to scrape.</p> <h3 id="dom-based-xss" tabindex="-1">DOM-based XSS</h3> <p>Finally, DOM-based XSS is when JavaScript running on the page uses data from somewhere the attacker can control, such as <code>window.location</code> (the URL of the page). Say we have some JS on our page which takes <code>window.location</code> and then executes some function with it:</p> <p><code>https://niche-tshirts.com/shirts?q=spelunking</code></p> <pre class="language-js"><code class="language-js"><span class="token keyword">const</span> params <span class="token operator">=</span> <span class="token keyword">new</span> <span class="token class-name">URLSearchParams</span><span class="token punctuation">(</span>document<span class="token punctuation">.</span>location<span class="token punctuation">.</span>search<span class="token punctuation">.</span><span class="token function">substring</span><span class="token punctuation">(</span><span class="token number">1</span><span class="token punctuation">)</span><span class="token punctuation">)</span> <span class="token keyword">const</span> searchTerm <span class="token operator">=</span> params<span class="token punctuation">.</span><span class="token function">get</span><span class="token punctuation">(</span><span class="token string">"q"</span><span class="token punctuation">)</span> document<span class="token punctuation">.</span><span class="token function">getElementById</span><span class="token punctuation">(</span><span class="token string">"content"</span><span class="token punctuation">)</span><span class="token punctuation">.</span>innerHTML <span class="token operator">=</span> <span class="token template-string"><span class="token template-punctuation string">`</span><span class="token string">&lt;p>You searched for: </span><span class="token interpolation"><span class="token interpolation-punctuation punctuation">${</span>searchTerm<span class="token interpolation-punctuation punctuation">}</span></span><span class="token string">&lt;/p></span><span class="token template-punctuation string">`</span></span></code></pre> <p>If our attacker from before sends another link with the <code>&lt;script&gt;</code> tags in the query string, these will be rendered into the DOM when the browser sets the inner HTML content of the <code>#content</code> element.</p> <p>Unlike the previous two examples, this all happens client-side - the server isn't involved at all.</p> <h3 id="preventing-xss" tabindex="-1">Preventing XSS</h3> <p>Some of the methods we can use to prevent XSS attacks on our websites include:</p> <ul> <li>sanitizing user input, making sure there are no HTML tags in it</li> <li>escape certain characters (like <code>&lt;</code>, <code>&gt;</code>, <code>&amp;</code>, and <code>&quot;</code>) with HTML entity encoding (e.g. <code>&amp;amp;</code> for &amp;) to prevent them being executed</li> <li>not rendering arbitrary content from the server as HTML - render it as plain text</li> <li>check the Content-Type is correct - only <code>text/html</code> if you are <em>definitely</em> returning HTML</li> <li>use <a href="https://developer.mozilla.org/en-US/docs/Web/HTTP/Cookies#Secure_and_HttpOnly_cookies">HttpOnly cookies</a> for sensitive data so that the JavaScript <code>Document.cookie</code> API can't access them</li> <li>and... set up a <code>Content-Security-Policy</code> header!</li> </ul> <h2 id="content-security-policy-is-a-set-of-rules-about-permitted-content-on-a-website" tabindex="-1">Content-Security-Policy is a set of rules about permitted content on a website</h2> <p><code>Content-Security-Policy</code> is an additional layer of security on a site, preventing things like malicious scripts from being run by defining strict rules about what content you can and can't have on a website. It can be one of the HTTP headers that we can send from the web server to the client (browser), or a meta tag in the <code>&lt;head&gt;</code> of an HTML page. A browser will read the CSP and check whether the scripts, stylesheets and various other resources it's executing or displaying conform to the rules in the policy. If not, it won't load them.</p> <figure><picture><source type="image/webp" srcset="https://localghost.dev/img/oCxeq0APZ_-280.webp 280w, https://localghost.dev/img/oCxeq0APZ_-640.webp 640w, https://localghost.dev/img/oCxeq0APZ_-841.webp 841w" sizes="auto"><img loading="lazy" decoding="async" src="https://localghost.dev/img/oCxeq0APZ_-280.jpeg" alt="Gandalf from Lord of the Rings saying 'you shall not pass'" width="841" height="398" srcset="https://localghost.dev/img/oCxeq0APZ_-280.jpeg 280w, https://localghost.dev/img/oCxeq0APZ_-640.jpeg 640w, https://localghost.dev/img/oCxeq0APZ_-841.jpeg 841w" sizes="auto"></picture><figcaption>CSP is basically Browser Gandalf.</figcaption></figure> <p>A CSP meta tag may look a bit like this, for a site with Google Analytics and some Twitter/Facebook embeds, and images from a CDN:</p> <pre class="language-html"><code class="language-html"><span class="token tag"><span class="token tag"><span class="token punctuation">&lt;</span>meta</span> <span class="token attr-name">http-equiv</span><span class="token attr-value"><span class="token punctuation attr-equals">=</span><span class="token punctuation">"</span>Content-Security-Policy<span class="token punctuation">"</span></span> <span class="token attr-name">content</span><span class="token attr-value"><span class="token punctuation attr-equals">=</span><span class="token punctuation">"</span>default-src 'none'; manifest-src 'none'; prefetch-src 'none'; worker-src 'none'; object-src 'self'; font-src *; connect-src 'self' https://www.google-analytics.com; img-src 'self' https://some-cdn.com; script-src 'self' https://platform.twitter.com https://www.google-analytics.com https://connect.facebook.net https://staticxx.facebook.com; style-src 'self' https://platform.twitter.com<span class="token punctuation">"</span></span><span class="token punctuation">></span></span></code></pre> <figure><picture><source type="image/webp" srcset="https://localghost.dev/img/9ZKJbyJ7OP-280.webp 280w, https://localghost.dev/img/9ZKJbyJ7OP-640.webp 640w" sizes="auto"><img loading="lazy" decoding="async" src="https://localghost.dev/img/9ZKJbyJ7OP-280.jpeg" alt="Console error: Refused to load the script 'http://evil.example.com/evil.js' because it violates the following Content Security Policy directive: script-src 'self' https://apis.google.com" width="640" height="156" srcset="https://localghost.dev/img/9ZKJbyJ7OP-280.jpeg 280w, https://localghost.dev/img/9ZKJbyJ7OP-640.jpeg 640w" sizes="auto"></picture><figcaption>An example Content-Security-Policy error from Google</figcaption></figure> <p>The CSP header or meta tag content is always a string containing a semicolon-separated list of rules.</p> <p>A policy has a list of <strong>directives</strong> which are usually suffixed with <code>-src</code>, and refer to different kinds of rules for resources and content on the page.<br> Some common categories of directive are:</p> <ul> <li><a href="https://localghost.dev/blog/defending-yourself-against-cross-site-scripting-attacks-with-content-security-policy/#fetch-directives">fetch directives</a>: which resource types may be loaded, and where from</li> <li><a href="https://localghost.dev/blog/defending-yourself-against-cross-site-scripting-attacks-with-content-security-policy/#document-directives">document directives</a>: what properties the document or worker may have</li> <li><a href="https://localghost.dev/blog/defending-yourself-against-cross-site-scripting-attacks-with-content-security-policy/#navigation-directives">navigation directives</a>: where a user on the page can navigate to/submit a form to</li> </ul> <p>Next to each directive we specify one or more permitted <strong>sources</strong> for each of these directives.</p> <pre class="language-html"><code class="language-html">script-src: 'self' https://othersite.com; <span class="token comment">&lt;!-- directive: source source; --></span></code></pre> <p>I won't go through every single directive - there are a lot - but I'll outline some of the most common ones, and what rules they might enforce. Plus, the good old caveat that Internet Explorer only supports <em>one</em> directive (<code>sandbox</code>), and that's with the legacy <code>X-Content-Security-Policy</code> header.</p> <p>While the HTTP header and meta tags are mostly the same, the main differences are that HTTP headers have wider browser support, support all of the directives (meta tags support <em>most</em> of the directives) and may be cached by proxies (meta tags won't be).</p> <p>As long as you're employing multiple methods of defence against XSS, including CSP, you can protect yourself (yes, <em>even</em> in IE). It's important to emphasise here that CSP is <em>one way</em> of protecting your website against XSS - it's not enough on its own.</p> <h3 id="fetch-directives" tabindex="-1">Fetch directives</h3> <p>These directives govern which resource types may be loaded, and where from. Things like images, scripts, styles and frames will be defined here.</p> <p>Any of these directives may also have a wildcard value (<code>*</code>), which basically means &quot;anything goes&quot;.</p> <p><code>image-src</code>: Permitted sources for any images/favicons on the page.</p> <p><code>script-src</code>: Permitted sources for any JavaScript on the page.</p> <p><code>style-src</code>: What kind of CSS it's allowed to load, and where from.</p> <p><code>font-src</code>: where fonts specified by the CSS <code>@font-face</code> rule may be fetched from (e.g. Google Fonts)</p> <p><code>frame-src</code>: where iframes may load content from (e.g. Stripe elements)</p> <p><code>connect-src</code>: hosts that your JavaScript may make requests to (e.g. an API, with <code>fetch</code>)</p> <p><code>default-src</code>: the fallback if any of the rules don't match the resource</p> <h3 id="document-directives" tabindex="-1">Document directives</h3> <p>Document directives restrict the use of plugins (such as with <code>&lt;embed&gt;</code>) and let us enable sandbox mode.</p> <p><code>plugin-types</code>: allows you to define permitted <a href="https://developer.mozilla.org/en-US/docs/Web/HTTP/Basics_of_HTTP/MIME_types/Common_types">MIME types</a> for embedded media and elements using <code>&lt;embed&gt;</code> tags. These days, you rarely need to use <code>embed</code>, so I don't expect you'll come across this one very much.</p> <p><code>sandbox</code>: literally the only directive that IE supports! This allows you to restrict the browser environment on the page, effectively blocking <em>everything</em> - popups, scripts, forms, you name it. It has different sources from the rest of the directives - you can specify exactly what should be enabled in sandbox mode. Take a look at the <a href="https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/Content-Security-Policy/sandbox">MDN documentation on sandbox</a> for more information.</p> <h3 id="navigation-directives" tabindex="-1">Navigation directives</h3> <p><code>form-action</code>: if you're using traditional HTML forms with <code>action</code> attributes, this directive specifies the URIs you're allowed to submit to.</p> <p><code>frame-ancestors</code>: specifies which pages or URIs are allowed to embed iframes and other frame/embeddable content.</p> <h3 id="sources" tabindex="-1">Sources</h3> <p>Most directives have the same possible list of sources. With the exception of <strong>host source</strong> and <strong>scheme source</strong>, they are all specified in single quotes.</p> <ul> <li><code>'self'</code> - anything from the same host is fine. So a CSP for localghost.dev with <code>image-src: 'self'</code> and <code>script-src: 'self'</code> would be allowed to display images and run scripts from localghost.dev.</li> <li>a <strong>host source</strong> : external hosts that the resource can come from. <ul> <li>For example, <code>style-src: &quot;https://myhost.com&quot;</code> allows you to include a stylesheet from <a href="http://myhost.com">myhost.com</a>, e.g. <code>&lt;link rel=&quot;stylesheet&quot; href=&quot;https://myhost.com/style.css&quot;/&gt;</code>.</li> <li>If you often use content delivery networks (CDNs), you'll need to specify them here. You can also use <strong>wildcards</strong> for the subdomain of the URL, and restrict things to specific <strong>ports</strong> as well. The <strong>protocol</strong> matters, so a host source with <code>https://</code> will not allow anything from <code>http://</code>.</li> <li>e.g. <code>connect-src: 'self' https://*.localghost.dev</code> will allow anything over HTTPS from a subdomain of this site.</li> </ul> </li> <li>a <strong>scheme source</strong>: what scheme (protocol) it's okay to fetch resources from. Chiefly <code>https:</code> or <code>http:</code>.</li> <li><code>'unsafe-inline'</code>: whether inline <code>&lt;style&gt;</code>/<code>&lt;script&gt;</code> tags are allowed. I've only really used this for <code>script-src</code> and <code>style-src</code> before. I'll talk more about why inline style and script tags are a security risk later in the article.</li> <li><code>'unsafe-eval'</code>: whether or not JS is allowed to use the <code>eval()</code> function, which executes arbitrary code from whatever string is passed in. I'll go into more detail about why this is a bad thing in a later section.</li> <li><code>nonce</code> - you may specify an <strong>unguessable</strong> cryptographic nonce (a base-64 encoded string) which you can then pass into inline styles or scripts as an attribute to allow them to load on the page. This means you can use inline <code>&lt;style nonce=&quot;mysecretstring&quot;&gt;</code> or <code>&lt;script&gt;</code> tags without indiscriminately allowing <em>all</em> inline styles - effectively marking only a specific set of inline styles as &quot;safe&quot;. This <strong>must</strong> be randomly generated and unguessable, otherwise it's basically pointless as anyone can guess the hash and pop it in their injected scripts.</li> <li>hashes - e.g. <code>sha256-&lt;your hash value&gt;</code>. This is a base64-encoded representation of your inline styles or scripts, so the browser can check the hash against its own hash of the <code>&lt;style&gt;</code>/<code>&lt;script&gt;</code> to make sure it's the real deal. Anything that doesn't match the hash will be ignored. Hashes are static, while nonces are generated server-side on every page load.</li> <li><code>none</code> - no URLs match, no nothing may be loaded at all. For example, I don't use iframes on this site at all, so I'd have <code>frame-src: none</code>.</li> </ul> <h3 id="unsafe-inline-and-the-risks-of-inline-styles" tabindex="-1"><code>unsafe-inline</code> and the risks of inline styles</h3> <p>But why would we have to explicitly enable <code>unsafe-inline</code> for <code>style-src</code>, and why are inline <code>&lt;style&gt;</code> tags considered &quot;unsafe&quot;?</p> <p>There are actually a few vulnerabilities that can be triggered using CSS. They're rare and, yes, less harmful than JS-based XSS attacks, but it's still worth protecting yourself against them.</p> <ul> <li>messing up your site's styles - on a big-name website or app, this can cause reputational damage</li> <li>injecting styles into the page to add unsolicited content or offensive text</li> <li>disguising user-generated content to look like official content, e.g. a fake login link</li> </ul> <p>All of these can be prevented with <code>unsafe-inline</code>, meaning the browser will only pay attention to the styles from your defined stylesheets.</p> <p>Ultimately, allowing <code>unsafe-inline</code> is a calculated risk: if you really can't get around it with a nonce or a hash, then you can enable this knowing that you are introducing a slight vulnerability, but it's highly unlikely to be anything super severe.</p> <p>There's a helpful <a href="https://stackoverflow.com/a/31759553">StackOverflow answer</a> which explains this in more detail.</p> <h3 id="scripts-unsafe-inline-and-unsafe-eval-why-eval-is-evil" tabindex="-1">Scripts, <code>unsafe-inline</code> and <code>unsafe-eval</code>: why <code>eval()</code> is evil</h3> <p>While enabling <code>unsafe-inline</code> for styles is not the end of the world, it's a bad idea to allow it for scripts. A lot of harm can be done with an inline script that's been injected into your code - with a <code>connect-src</code> directive defined as well it would mean that that script couldn't connect to any other sources, but it could still cause havoc, such as pretending to be you and updating user-generated content.</p> <p><code>eval()</code>, on the other hand, should be avoided at all costs. It takes a string as a parameter, and blindly executes it as Javascript. There's literally a section in the MDN article about <code>eval()</code> called <a href="https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/eval#Never_use_eval!">&quot;Never use eval()!&quot;</a>. This opens up a whole host of security vulnerabilities - if <code>unsafe-eval</code> is enabled, anyone can execute arbitrary code in your application. Yikes.</p> <h2 id="how-to-create-a-csp-header" tabindex="-1">How to create a CSP header</h2> <p>In a static site, you can add a meta tag to your site's <code>&lt;head&gt;</code>:</p> <pre class="language-html"><code class="language-html"><span class="token tag"><span class="token tag"><span class="token punctuation">&lt;</span>meta</span> <span class="token attr-name">http-equiv</span><span class="token attr-value"><span class="token punctuation attr-equals">=</span><span class="token punctuation">"</span>Content-Security-Policy<span class="token punctuation">"</span></span> <span class="token attr-name">content</span><span class="token attr-value"><span class="token punctuation attr-equals">=</span><span class="token punctuation">"</span>default-src 'none'; manifest-src 'none'; prefetch-src 'none'; worker-src 'none'; object-src 'self'; font-src *; connect-src 'self' https://www.google-analytics.com; img-src 'self' https://some-cdn.com; script-src 'self' 'sha256-flsjfljlkfjaspdjfsdkgs' https://platform.twitter.com https://www.google-analytics.com https://connect.facebook.net https://staticxx.facebook.com; style-src 'self' 'nonce-adsfasdfasfsadf' https://platform.twitter.com<span class="token punctuation">"</span></span><span class="token punctuation">></span></span></code></pre> <p>If you're running an app with an Express server, you can easily set it as one of the response headers with <code>helmet-csp</code> - check out the <a href="https://helmetjs.github.io/docs/csp/">Helmet docs</a> for more information.</p> <p>The safest method is to have the most restrictive CSP possible, and only add new sources if you're completely sure. Include a <code>default-src: 'none'</code> so that if no rule matches a resource, it will be blocked.</p> <h2 id="adding-to-a-csp-header" tabindex="-1">Adding to a CSP header</h2> <p>When is it safe to add new sources to a CSP header? Perhaps you've included an image or stylesheet in your code, and your browser is refusing to render it because it violates the CSP. Before adding the URL as a source, consider:</p> <ul> <li>do you trust this site?</li> <li>how specific can you make the URI? (avoid wildcards unless it's a host you control)</li> <li>could you host the file on an already approved source?</li> <li>could you add the CSS file to your repo instead?</li> </ul> <h3 id="styled-components-and-unsafe-inline" tabindex="-1">styled-components and <code>unsafe-inline</code></h3> <p>If you're using <code>styled-components</code>, which renders <code>&lt;style&gt;</code> tags into the page, rather than enabling <code>unsafe-inline</code> for styles you can define a nonce by setting it as a Webpack global (<code>__webpack_nonce__</code>). The caveats: this is apparently undocumented, and only works with server-side rendered code. So you might have to enable <code>unsafe-inline</code> for that one.</p> <h3 id="environment-based-csp" tabindex="-1">Environment-based CSP</h3> <p>If you run different environments for development and production, you may want to consider serving different CSP headers for different environments. For example, your <code>image-src</code> directive might point to a non-prod CDN if <code>process.env.NODE_ENV !== 'production</code>, but the production CDN otherwise. Similarly, if you use a local server at <code>localhost:6060</code> as an API for development but your production app points to a hosted API somewhere else, you might want to add <code>localhost:6060</code> to your CSP <em>only for the development environment</em>.</p> <p>There are some frameworks, such as Hugo and Next.JS, which rely on inline scripts for hot reloading. In this case, it's fine to add <code>unsafe-inline</code> to your <code>script-src</code> <strong>in development</strong>.</p> <p>For example, if you use Hugo and want to enable live reloading with your CSP, as well as scripts imported from the site itself, you can use <code>.Site.IsServer</code> (though this only works if you use server mode for development and not production):</p> <pre class="language-html"><code class="language-html"><span class="token tag"><span class="token tag"><span class="token punctuation">&lt;</span>meta</span> <span class="token attr-name">http-equiv</span><span class="token attr-value"><span class="token punctuation attr-equals">=</span><span class="token punctuation">"</span>Content-Security-Policy<span class="token punctuation">"</span></span> <span class="token attr-name">content</span><span class="token attr-value"><span class="token punctuation attr-equals">=</span><span class="token punctuation">"</span>default-src 'none'; [...] script-src 'self' {{ if eq .Site.IsServer true }}'unsafe-inline'{{ end}} {{ .Site.BaseURL }};<span class="token punctuation">"</span></span><span class="token punctuation">></span></span></code></pre> <h2 id="the-moral-of-the-story" tabindex="-1">The moral of the story</h2> <p>So, how did I get around the <code>unsafe-eval</code> warnings I was seeing in the console?</p> <figure><picture><source type="image/webp" srcset="https://localghost.dev/img/SdVazBrcsk-245.webp 245w"><img loading="lazy" decoding="async" src="https://localghost.dev/img/SdVazBrcsk-245.jpeg" alt="A gif of Ron Swanson throwing his computer into a dumpster" width="245" height="169"></picture></figure> <p>Unfortunately... the only solution was to chuck out that library and find something else. There are no workarounds for a library that uses <code>eval</code>.</p> <p>When adding a CSP header to a site, start out with the principle of least privilege: only permit <em>exactly</em> what you need, nothing more. When in doubt, be overly restrictive, and see what console errors you're getting.</p> <p>If you're working on a site or app that has a CSP header set, don't be tempted to add sources just to make console errors go away. Make sure you know exactly what you're allowing.</p> <p>And promise me you will never, ever enable <code>unsafe-eval</code>.</p> <h2 id="references-and-further-reading" tabindex="-1">References &amp; further reading</h2> <p><a href="https://www.cloudflare.com/learning/security/threats/cross-site-scripting/">Cloudflare - What is Cross-Site Scripting?</a></p> <p><a href="https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/Content-Security-Policy">MDN - Content-Security-Policy</a></p> <p><a href="https://developer.mozilla.org/en-US/docs/Glossary/Cross-site_scripting">MDN - Cross-Site Scripting</a></p> <p><a href="https://owasp.org/www-community/attacks/xss/">OWASP - Cross-Site Scripting</a></p> <p><a href="https://owasp.org/www-community/attacks/DOM_Based_XSS">OWASP - DOM-based XSS</a></p> <p>Google's <a href="https://csp-evaluator.withgoogle.com/">CSP Evaluator</a> can check how watertight your CSP is (but is not exhaustive).</p> <p><a href="https://developers.google.com/web/fundamentals/security/csp">Google Web Fundamentals - CSP</a></p> ffconf 2019: future friends beside the seaside 2019-11-16T00:00:00Z https://localghost.dev/blog/ffconf-2019-future-friends-beside-the-seaside/ <p>Now in its 11th year, <a href="https://ffconf.org">ffconf</a> is one of the biggest events in the conference calendar for web developers across the UK (and further afield). Yet somehow I've managed to miss every one since I got into tech, because of some reason or another - last year I was at the week-long blockchain sales pitch that is Web Summit - so I was understandably very excited to finally be going. It takes place at the lovely <a href="https://www.picturehouses.com/cinema/duke-of-york-s-picturehouse">Duke of York's Picturehouse</a> in Brighton - the first conference I've attended that had cupholders in the seats! (Fun fact, I applied for a job there once.)</p> <figure><picture><source type="image/webp" srcset="https://localghost.dev/blog/ffconf-2019-future-friends-beside-the-seaside/4oRIYi_FYn-280.webp 280w, https://localghost.dev/blog/ffconf-2019-future-friends-beside-the-seaside/4oRIYi_FYn-400.webp 400w" sizes="auto"><img loading="lazy" decoding="async" src="https://localghost.dev/blog/ffconf-2019-future-friends-beside-the-seaside/4oRIYi_FYn-280.jpeg" alt="" width="400" height="267" srcset="https://localghost.dev/blog/ffconf-2019-future-friends-beside-the-seaside/4oRIYi_FYn-280.jpeg 280w, https://localghost.dev/blog/ffconf-2019-future-friends-beside-the-seaside/4oRIYi_FYn-400.jpeg 400w" sizes="auto"></picture><figcaption>Photo: Trys Mudford</figcaption></figure> <p>One of the best things about attending conferences is seeing friendly faces from the community, and this particular subset of the JS community is so lovely. It does give you hope (and a refreshing break from some of the other conferences and meetups I've attended in the past few years which have had 97% more posturing). I spend a lot of time in the London bubble so it's great to meet folks from other tech communities, and going to the <a href="https://asyncjs.com/">Async</a> meetup the night before was a great way to do that.</p> <figure><picture><source type="image/webp" srcset="https://localghost.dev/blog/ffconf-2019-future-friends-beside-the-seaside/PeOAHWuqX2-280.webp 280w, https://localghost.dev/blog/ffconf-2019-future-friends-beside-the-seaside/PeOAHWuqX2-400.webp 400w" sizes="auto"><img loading="lazy" decoding="async" src="https://localghost.dev/blog/ffconf-2019-future-friends-beside-the-seaside/PeOAHWuqX2-280.jpeg" alt="" width="400" height="267" srcset="https://localghost.dev/blog/ffconf-2019-future-friends-beside-the-seaside/PeOAHWuqX2-280.jpeg 280w, https://localghost.dev/blog/ffconf-2019-future-friends-beside-the-seaside/PeOAHWuqX2-400.jpeg 400w" sizes="auto"></picture><figcaption>JS (and real-life) friends! (photo: Trys Mudford)</figcaption></figure> <p>(I also got to meet <a href="https://noopkat.com">Suz Hinton</a> at the afterparty and didn't make a fool of myself, so that is always a plus. She's extremely lovely.)</p> <p>What made these talks <em>especially</em> good was that they focused more on people and stories. My favourite kind of conference talk is one where people show something amazing they built, and share their experiences of building it (a good example from JSConf EU 2019 is <a href="https://www.youtube.com/watch?v=ZsBAkSxwU5c">Bringing back dialup: the internet over SMS</a> by Alexandra Sunderland). I don't need to come away with an exact step-by-step guide on how to do the thing (I'll never remember it all anyway), and it might not even be a practical thing for me to ever attempt, but I want to know all about it because it's amazing.</p> <p>I've been thinking a lot recently about the <em>reasons</em> that people learn to code - and in fact, I submitted to the ffconf CFP with a talk about this, which I'm going to try and polish up and submit elsewhere. When I started doing web development, I didn't really know what I was doing or why I was doing it - I thought it'd be cool to make a website, and how else was I going to get my Neopets shop to play music and have a flashy background?</p> <p>It seems like a lot of the coding for creativity's sake has been lost in favour of learning the latest, greatest technology in order to advance your career and to be able to say that you're using it. I could go on, and I probably will in another blog post, but seeing people up on stage doing ridiculous and beautiful things with code gives me hope for creative coding (and, most importantly, it inspires me to do some too).</p> <p>I came home from ffconf and restyled my entire website with ludicrous link effects and a gradient border. It's not quite making jewellery from SVGs but it's a start.</p> <p>Here's a bit of what I took away from each of the ffconf talks. I haven't included many photos because my phone's camera is atrocious in low light, but I've borrowed some nice pictures from the official photos by Trys Mudford on <a href="https://flickr.com/photos/remysharp/albums/72157711751196107">Remy Sharp's Flickr</a>.</p> <h2 id="sharon-steed-engaging-empathy" tabindex="-1">Sharon Steed - Engaging Empathy</h2> <blockquote> <p>&quot;Empathy: the thing you have to do to not be an asshole in the office.&quot;</p> </blockquote> <p>Opening the conference was author and communications consultant Sharon Steed. She spoke about her experiences of growing up with and living with a stutter - and the difficulties she's had with others not taking her seriously. She's a fantastic storyteller, and as she shared with us her challenges and achievements it felt like she was taking us with her on her journey.</p> <figure><picture><source type="image/webp" srcset="https://localghost.dev/blog/ffconf-2019-future-friends-beside-the-seaside/FdEFyJN9rq-280.webp 280w, https://localghost.dev/blog/ffconf-2019-future-friends-beside-the-seaside/FdEFyJN9rq-400.webp 400w" sizes="auto"><img loading="lazy" decoding="async" src="https://localghost.dev/blog/ffconf-2019-future-friends-beside-the-seaside/FdEFyJN9rq-280.jpeg" alt="" width="400" height="267" srcset="https://localghost.dev/blog/ffconf-2019-future-friends-beside-the-seaside/FdEFyJN9rq-280.jpeg 280w, https://localghost.dev/blog/ffconf-2019-future-friends-beside-the-seaside/FdEFyJN9rq-400.jpeg 400w" sizes="auto"></picture><figcaption>Sharon Steed (photo: Trys Mudford)</figcaption></figure> <p>She compared empathy to love; &quot;the thing everyone understands, but nobody can explain&quot;. It's intangible, but it's also a choice: we can <em>choose</em> to have empathy for others (and we should). (Her tales of new love were accompanied by <em>that</em> picture of Britney and Justin in denim, which was an all around win for me.)</p> <figure><picture><source type="image/webp" srcset="https://localghost.dev/img/BKiagcc3gE-280.webp 280w, https://localghost.dev/img/BKiagcc3gE-640.webp 640w, https://localghost.dev/img/BKiagcc3gE-960.webp 960w" sizes="auto"><img loading="lazy" decoding="async" src="https://localghost.dev/img/BKiagcc3gE-280.jpeg" alt="Photo of Sharon Steed giving a conference talk" width="960" height="674" srcset="https://localghost.dev/img/BKiagcc3gE-280.jpeg 280w, https://localghost.dev/img/BKiagcc3gE-640.jpeg 640w, https://localghost.dev/img/BKiagcc3gE-960.jpeg 960w" sizes="auto"></picture></figure> <p>So, what to take away from Sharon's talk? We should <strong>foster environments of collaboration and inclusion</strong>. People need to feel safe in order to speak honestly - having open lines of communication and being transparent from the start will help this. She also pointed out that teams don't always need to work in the same way: people should be able to do things on their own terms.</p> <p>Sharon also described what she called the &quot;Key Empathy Behaviours&quot;: patience, perspective and connection. As long as we are being present and remembering the <strong>why</strong> of what we're doing, understanding where others are coming from, and speak for <strong>intention</strong> and not impact, we'll be able to engage empathy and make everyone feel like they belong.</p> <p>ffconf talk page: <a href="https://ffconf.org/talks/empathy/">Engaging Empathy</a></p> <h2 id="amina-adewusi-what-does-it-take-to-become-a-developer-in-2020" tabindex="-1">Amina Adewusi - What does it take to become a developer in 2020?</h2> <blockquote> <p>&quot;We need a recruitment revolution.&quot;</p> </blockquote> <p>Amina's talk focused on the barriers to entry for under-represented people in the industry: a topic that has come up time and time again but doesn't really seem to be getting any better (especially for people of colour).</p> <figure><picture><source type="image/webp" srcset="https://localghost.dev/blog/ffconf-2019-future-friends-beside-the-seaside/fPI2-eGNYe-280.webp 280w, https://localghost.dev/blog/ffconf-2019-future-friends-beside-the-seaside/fPI2-eGNYe-400.webp 400w" sizes="auto"><img loading="lazy" decoding="async" src="https://localghost.dev/blog/ffconf-2019-future-friends-beside-the-seaside/fPI2-eGNYe-280.jpeg" alt="" width="400" height="267" srcset="https://localghost.dev/blog/ffconf-2019-future-friends-beside-the-seaside/fPI2-eGNYe-280.jpeg 280w, https://localghost.dev/blog/ffconf-2019-future-friends-beside-the-seaside/fPI2-eGNYe-400.jpeg 400w" sizes="auto"></picture><figcaption>Amina Adewusi (photo: Trys Mudford)</figcaption></figure> <p>Her words were powerful, and the measured way she spoke really conveyed the gravity of what she was saying. In the last 11 years, the proportion of women and non-white students in computer science has not improved: last year, only 20% of computer science students were female, and only 15% were non-white. And a computer science degree is already a privileged platform to launch your career from. What about those who don't go down that path?</p> <p>She spoke of the generosity of strangers: the bootcamp alumni who all answered her questions when she contacted them out of the blue, happy to give advice. And while bootcamps are a great way to fast-track your career into tech, they're also very exclusionary for anyone with caring responsibilities, and they can be very expensive. She spoke of her experiences raising a baby while also learning to code, which frankly blew my mind - learning to code is really hard (don't let anyone tell you otherwise!) so to do that while also looking after a very young child is just phenomenal.</p> <figure><picture><source type="image/webp" srcset="https://localghost.dev/img/QUFQx_N-0j-280.webp 280w, https://localghost.dev/img/QUFQx_N-0j-640.webp 640w, https://localghost.dev/img/QUFQx_N-0j-960.webp 960w" sizes="auto"><img loading="lazy" decoding="async" src="https://localghost.dev/img/QUFQx_N-0j-280.jpeg" alt="Photo of Amina Adewusi giving a conference talk" width="960" height="664" srcset="https://localghost.dev/img/QUFQx_N-0j-280.jpeg 280w, https://localghost.dev/img/QUFQx_N-0j-640.jpeg 640w, https://localghost.dev/img/QUFQx_N-0j-960.jpeg 960w" sizes="auto"></picture></figure> <p>For those who self-teach, it's hard to get a job once you've learned the skills. More often than not, recruitment is unforgiving, with high expectations, long interview processes and unreasonable deadlines (Amina recalled being sent a tech test on a Friday, with the expectation she'd send it back on the Monday!). It's not just the time it takes, either - the interview process can be very intimidating and sometimes discriminatory.</p> <p>As Amina put it: <strong>this is a call to action</strong>. How can we, as experienced engineers, help the next generation of developers get a foothold? Some of her excellent suggestions:</p> <ul> <li>mentor at organisations like <a href="https://www.codebar.io/">Codebar</a></li> <li>pair with new engineers</li> <li>give engineers time to work on open source</li> <li>be mindful when recruiting - don't assume everyone has all the time in the world</li> <li>observe the <a href="https://www.ericholscher.com/blog/2017/aug/2/pacman-rule-conferences/">&quot;pacman&quot; rule</a> when talking to people at events</li> </ul> <p>ffconf talk page: <a href="https://ffconf.org/talks/what-does-it-take/">What does it take to become a developer in 2020?</a></p> <h2 id="alice-bartlett-getting-more-from-git" tabindex="-1">Alice Bartlett - Getting more from Git</h2> <blockquote> <p>&quot;My process is 'try stuff until it works and don't ask any questions'.&quot;</p> </blockquote> <p>Somehow, Alice turned a pretty dry subject - git - into one of the most relatable and entertaining talks I've ever seen.</p> <figure><picture><source type="image/webp" srcset="https://localghost.dev/blog/ffconf-2019-future-friends-beside-the-seaside/WsXF23dE69-280.webp 280w, https://localghost.dev/blog/ffconf-2019-future-friends-beside-the-seaside/WsXF23dE69-400.webp 400w" sizes="auto"><img loading="lazy" decoding="async" src="https://localghost.dev/blog/ffconf-2019-future-friends-beside-the-seaside/WsXF23dE69-280.jpeg" alt="" width="400" height="267" srcset="https://localghost.dev/blog/ffconf-2019-future-friends-beside-the-seaside/WsXF23dE69-280.jpeg 280w, https://localghost.dev/blog/ffconf-2019-future-friends-beside-the-seaside/WsXF23dE69-400.jpeg 400w" sizes="auto"></picture><figcaption>Alice Bartlett (photo: Trys Mudford)</figcaption></figure> <p>With entertaining anecdotes about the meaningless commit messages she'd write early in her career (we've all been guilty of that, and you should see the nonsense commits I've made to this very website) she highlighted the importance of considering who else is going to be reading all of it - including Future You. A commit message should be a single unit of work that makes sense in isolation, and that will tell you <em>why</em> the change is made. So no more &quot;fix bug&quot; commits.</p> <p>Of course, it's not always possible to commit code with this in mind, and many of us approach coding by trying stuff out and going all over the place, so git offers us some handy ways of rewriting history so that we can make the commits sensible <em>after</em> the fact.</p> <p>Here, Alice took us through a brief history of git (including a very entertaining swipe at Linus Torvalds and his <a href="https://gizmodo.com/linux-founder-takes-some-time-off-to-learn-how-to-stop-1829105667">controversy</a>: &quot;the creator of Linux and git, two projects he named after himself&quot;). Many attendees in the room hadn't had the... pleasure of working with non-distributed source control systems like SVN, so she took us through how git differs and why it's so helpful for things like testing out changes and controlling the quality of code that's merged in.</p> <blockquote> <p>&quot;Why are we so bad at git?&quot;</p> </blockquote> <figure><picture><source type="image/webp" srcset="https://localghost.dev/img/_6yJdOKC8S-280.webp 280w, https://localghost.dev/img/_6yJdOKC8S-640.webp 640w, https://localghost.dev/img/_6yJdOKC8S-960.webp 960w" sizes="auto"><img loading="lazy" decoding="async" src="https://localghost.dev/img/_6yJdOKC8S-280.jpeg" alt="Picture of LEGO heads that says 'you are in a detached HEAD state'" width="960" height="720" srcset="https://localghost.dev/img/_6yJdOKC8S-280.jpeg 280w, https://localghost.dev/img/_6yJdOKC8S-640.jpeg 640w, https://localghost.dev/img/_6yJdOKC8S-960.jpeg 960w" sizes="auto"></picture><figcaption>Everything is confusing</figcaption></figure> <p>A lot of the problem is the jargon and the impenetrable docs, Alice argued - and I agree with her there, having never read a man page that I understood.</p> <p>And then, using simple and clear language, Alice took us through the process of rewriting git history using interactive rebase (<code>git rebase --i</code>). It's a super useful tool, allowing you to squash multiple commits into one, reword commit messages and change the order they are applied in. She also showed us <code>git add --patch</code> which lets you choose which &quot;hunks&quot; of your code to commit and which to leave unstaged. I'm not going to go into too much detail here, but the video is definitely worth a watch when it comes out.</p> <p>Some top git tips here to take away:</p> <ul> <li>Don't just link to Slack posts or tickets in the PR - give a bit of background info</li> <li>...but don't write an essay about the change, keep it concise</li> <li>commit messages should be meaningful and say <em>why</em> the change happened</li> </ul> <p>ffconf talk page: <a href="https://ffconf.org/talks/getting-more-from-git/">Getting more from Git</a></p> <h2 id="laura-kalbag-8-unbelievable-things-you-never-knew-about-tracking" tabindex="-1">Laura Kalbag - 8 Unbelievable Things You Never Knew About Tracking</h2> <blockquote> <p>&quot;We're asked to give up everything, or get nothing.&quot;</p> </blockquote> <p>Laura highlighted the ridiculous amount of information that various sites have about all of us just through embedded scripts and images. And even though this data is anonymised, a study showed it was possible to actually re-identify people from this data based on their browsing habits. Scary, right?</p> <p><picture><source type="image/webp" srcset="https://localghost.dev/blog/ffconf-2019-future-friends-beside-the-seaside/EYCWm2YqDi-280.webp 280w, https://localghost.dev/blog/ffconf-2019-future-friends-beside-the-seaside/EYCWm2YqDi-400.webp 400w" sizes="auto"><img loading="lazy" decoding="async" src="https://localghost.dev/blog/ffconf-2019-future-friends-beside-the-seaside/EYCWm2YqDi-280.jpeg" alt="" title="Laura Kalbag (photo: Trys Mudford" width="400" height="267" srcset="https://localghost.dev/blog/ffconf-2019-future-friends-beside-the-seaside/EYCWm2YqDi-280.jpeg 280w, https://localghost.dev/blog/ffconf-2019-future-friends-beside-the-seaside/EYCWm2YqDi-400.jpeg 400w" sizes="auto"></picture>)</p> <p>She touched on the Cambridge Analytica scandal - &quot;tracking affects democracy&quot; - and how companies can manipulate people through precise targeting based on their interests, knowing exactly what will make them tick. Facebook's latest ad campaign got a shout out for being totally hypocritical - when you choose to share something with &quot;Only Me&quot; or &quot;Friends&quot; you're also sharing it with Facebook!</p> <figure><picture><source type="image/webp" srcset="https://localghost.dev/img/8fVrVjpP3M-280.webp 280w, https://localghost.dev/img/8fVrVjpP3M-640.webp 640w, https://localghost.dev/img/8fVrVjpP3M-960.webp 960w" sizes="auto"><img loading="lazy" decoding="async" src="https://localghost.dev/img/8fVrVjpP3M-280.jpeg" alt="Photo of Laura Kalbag giving a conference talk" width="960" height="704" srcset="https://localghost.dev/img/8fVrVjpP3M-280.jpeg 280w, https://localghost.dev/img/8fVrVjpP3M-640.jpeg 640w, https://localghost.dev/img/8fVrVjpP3M-960.jpeg 960w" sizes="auto"></picture></figure> <p>Companies make it worse by having incomprehensible terms and conditions, filled with legal jargon. When you consider that 1 in 6 people in England have very poor literacy skills (source: <a href="https://literacytrust.org.uk/parents-and-families/adult-literacy/">National Literacy Trust</a>) it's especially problematic. Sites like Facebook are a lifeline for many people, but if they can't understand what they're signing up for is it really consent?</p> <p>While we can't escape technology - Laura argued there is no longer a distinction between &quot;online life&quot; and &quot;real life&quot; - we must all consider the <strong>impact our products will have</strong> outisde of the immediate interface. She described how we can build more ethical tech, by making things that function without users' personal info, furthering the human experience rather than for corporate profit. I find this a really interesting and difficult dilemma, as in our society it's really difficult (if not impossible) to build a sustainable product that isn't for-profit in some way. It would require pretty significant government support, which isn't going to happen any time soon.</p> <blockquote> <p>&quot;Friends don't let friends get tracked by corporations.&quot;</p> </blockquote> <p>I knew from the title that this talk would be worrying, but I didn't anticipate just how unsettled I'd feel afterwards! As someone whose life has been basically entirely managed by Google products for years, I realised I needed to make a change. Laura suggested <a href="https://switching.software">switching.software</a> as a resource for alternatives to common &quot;big tech&quot; products, which is a great place to start.</p> <p>ffconf talk page: <a href="https://ffconf.org/talks/privacy/">8 Unbelievable Things You Never Knew About Tracking</a></p> <h2 id="harry-roberts-from-milliseconds-to-millions-a-look-at-the-numbers-powering-web-performance" tabindex="-1">Harry Roberts - From Milliseconds to Millions: A Look at the Numbers Powering Web Performance</h2> <blockquote> <p>&quot;How do you <em>know</em> the site is slow?&quot;</p> </blockquote> <p>A talk with a rather different tone from the last one - a focus on Harry's experience as a web performance consultant. Performance is definitely one of those things I know I should know more about, so I was geared up for the next couple of talks to help me boost that knowledge a little bit.</p> <figure><picture><source type="image/webp" srcset="https://localghost.dev/blog/ffconf-2019-future-friends-beside-the-seaside/h6_CA39CjY-280.webp 280w, https://localghost.dev/blog/ffconf-2019-future-friends-beside-the-seaside/h6_CA39CjY-400.webp 400w" sizes="auto"><img loading="lazy" decoding="async" src="https://localghost.dev/blog/ffconf-2019-future-friends-beside-the-seaside/h6_CA39CjY-280.jpeg" alt="" width="400" height="267" srcset="https://localghost.dev/blog/ffconf-2019-future-friends-beside-the-seaside/h6_CA39CjY-280.jpeg 280w, https://localghost.dev/blog/ffconf-2019-future-friends-beside-the-seaside/h6_CA39CjY-400.jpeg 400w" sizes="auto"></picture><figcaption>Harry Roberts (photo: Trys Mudford)</figcaption></figure> <p>Harry had the great suggestion of keeping a list of questions that you can ask all potential clients, so that you can better understand the problem. In his case, things like: &quot;How do you know the site is slow?&quot;. You could do this for any kind of consulting work - knowing exactly what to ask is hugely important. You can shape these questions over time, knowing exactly where to look next time and what doesn't work.</p> <p>I must admit I was a little put off when he suggested we &quot;leave our ethics at the door&quot; given the talk he'd just followed, and some of the talk seemed a little too focused on the clients and not on the stories behind it. But this was, after all, a talk on numbers.</p> <p>I found it particularly interesting how he identified a performance issue localised to Venezuela, and got all the engineers on the team to use a simulated Venezuelan internet connection to get them to think about things like network conditions and how it might be for other countries. We often take our internet speed for granted, and even when Virgin Media is at its worst, we still have vastly better internet than much of the world. Much of the world's population is dependent on mobile internet, so are our apps really suitable for everyone?</p> <p>Harry advocated for normalising performance - making it part of our everyday work. It can be a hard thing to argue for, when your managers are pushing for more new features, but if you can show them the numbers, it's hard to argue.</p> <p>ffconf talk page: <a href="https://ffconf.org/talks/from-milliseconds-to-millions/">From Milliseconds to Millions: A Look at the Numbers Powering Web Performance</a></p> <h2 id="anna-migas-effortless-performance-debugging" tabindex="-1">Anna Migas - Effortless Performance Debugging</h2> <blockquote> <p>&quot;If [the browser] fails to load the frames fast enough, we'll experience jank.&quot;</p> </blockquote> <p>Continuing my crash course in performance, Anna showed us with a live demo how we can use the power of Chrome Dev Tools to debug some common performance issues. Using the Profiler tab she showed how we can identify how loading is progressing and the performance of animations during interactions. You can see at-a-glance what's taking the longest time to load, plus it also has some helpful metrics such as the time to <a href="https://developers.google.com/web/tools/lighthouse/audits/first-meaningful-paint">First Meaningful Paint</a>.</p> <figure><picture><source type="image/webp" srcset="https://localghost.dev/blog/ffconf-2019-future-friends-beside-the-seaside/UPORR7LOw1-280.webp 280w, https://localghost.dev/blog/ffconf-2019-future-friends-beside-the-seaside/UPORR7LOw1-400.webp 400w" sizes="auto"><img loading="lazy" decoding="async" src="https://localghost.dev/blog/ffconf-2019-future-friends-beside-the-seaside/UPORR7LOw1-280.jpeg" alt="" width="400" height="267" srcset="https://localghost.dev/blog/ffconf-2019-future-friends-beside-the-seaside/UPORR7LOw1-280.jpeg 280w, https://localghost.dev/blog/ffconf-2019-future-friends-beside-the-seaside/UPORR7LOw1-400.jpeg 400w" sizes="auto"></picture><figcaption>Anna Migas (photo: Trys Mudford)</figcaption></figure> <p>In the spirit of the Live Demo, Anna was going to show us how the Lighthouse integration in Chrome can audit performance issues on web apps, but sadly technology was not on our side and Lighthouse wouldn't work (it seemed it wasn't just Anna though). Despite this hiccup, there were still a lot of really useful tips I took away from her talk:</p> <ul> <li>Look through Network tab to see what's been downloaded, and in what order</li> <li>Block elements using the request blocking tab to see the impact of not loading them</li> <li>Using <code>font-display: swap</code> to show a fallback font before the font is loaded</li> <li>Using a lazy-loading library to only load pictures as you need them</li> <li>Using an image's intrinsic ratio to show placeholders and prevent text from jumping around as images load</li> <li>Splitting CSS into a critical path stylesheet and others that can be loaded separately</li> </ul> <figure><picture><source type="image/webp" srcset="https://localghost.dev/img/KBLkfOcRJL-280.webp 280w, https://localghost.dev/img/KBLkfOcRJL-640.webp 640w, https://localghost.dev/img/KBLkfOcRJL-960.webp 960w" sizes="auto"><img loading="lazy" decoding="async" src="https://localghost.dev/img/KBLkfOcRJL-280.jpeg" alt="Photo of Anna Migas giving a conference talk" width="960" height="725" srcset="https://localghost.dev/img/KBLkfOcRJL-280.jpeg 280w, https://localghost.dev/img/KBLkfOcRJL-640.jpeg 640w, https://localghost.dev/img/KBLkfOcRJL-960.jpeg 960w" sizes="auto"></picture></figure> <p>ffconf talk page: <a href="https://ffconf.org/talks/effortless-performance-debugging/">Effortless Performance Debugging</a></p> <h2 id="charlotte-dann-taking-the-web-off-the-screen" tabindex="-1">Charlotte Dann - Taking The Web Off The Screen</h2> <blockquote> <p>&quot;You'd think that art is an expressive process, and adding this binary middle step seems incongruous, but... computers are better adapted to the design process than any traditional artistic medium.&quot;</p> </blockquote> <p>I actually didn't make many notes for this talk because I was so engrossed in the incredible visuals Charlotte was showing us. As I mentioned before, I'm a big fan of coding for artistic reasons, even if I don't really do it myself much. I'm not particularly &quot;good&quot; at SVG or any kind of web animation, so it blows me away when I see what people can do with canvas, SVG and JS.</p> <figure><picture><source type="image/webp" srcset="https://localghost.dev/blog/ffconf-2019-future-friends-beside-the-seaside/9ni0Xbx7yR-280.webp 280w, https://localghost.dev/blog/ffconf-2019-future-friends-beside-the-seaside/9ni0Xbx7yR-400.webp 400w" sizes="auto"><img loading="lazy" decoding="async" src="https://localghost.dev/blog/ffconf-2019-future-friends-beside-the-seaside/9ni0Xbx7yR-280.jpeg" alt="" width="400" height="267" srcset="https://localghost.dev/blog/ffconf-2019-future-friends-beside-the-seaside/9ni0Xbx7yR-280.jpeg 280w, https://localghost.dev/blog/ffconf-2019-future-friends-beside-the-seaside/9ni0Xbx7yR-400.jpeg 400w" sizes="auto"></picture><figcaption>Charlotte Dann (photo: Trys Mudford)</figcaption></figure> <p>In Charlotte's case, she bridges the gap between digital and physical art, using what she designs using generative CSS to produce prints and even intricate jewellery. She's made jigsaw puzzles which she's laser cut out, got a pen plotter to draw abstract designs onto pieces of paper for that precise yet hand-drawn look.</p> <p>Using <code>nth-child</code> pseudoselectors, she showed us how you can create an abstract pattern from a series of squares and circles. She's done a few of these, and used them as wall art for her studio!</p> <figure><picture><source type="image/webp" srcset="https://localghost.dev/img/28ycjG1R-z-280.webp 280w, https://localghost.dev/img/28ycjG1R-z-640.webp 640w, https://localghost.dev/img/28ycjG1R-z-960.webp 960w" sizes="auto"><img loading="lazy" decoding="async" src="https://localghost.dev/img/28ycjG1R-z-280.jpeg" alt="Photo of Charlotte Dann giving a conference talk" width="960" height="719" srcset="https://localghost.dev/img/28ycjG1R-z-280.jpeg 280w, https://localghost.dev/img/28ycjG1R-z-640.jpeg 640w, https://localghost.dev/img/28ycjG1R-z-960.jpeg 960w" sizes="auto"></picture></figure> <p>She's also the founder of <a href="https://hexatope.io/">Hexatope</a>, a web app that lets you draw beautiful patterns on a hexagonal grid and get them cast into silver and gold jewellery. The designs are incredible, and it's really fun to play with.</p> <p>I absolutely love hearing about things like this, and it's inspired me to get more involved in creative coding. If only people spent as much time doing things like this as they did arguing about which framework is best.</p> <p>ffconf talk page: <a href="https://ffconf.org/talks/taking-the-web-off-the-screen/">Taking The Web Off The Screen</a></p> <h2 id="suz-hinton-adventures-in-reinventing-interfaces" tabindex="-1">Suz Hinton - Adventures in reinventing interfaces</h2> <blockquote> <p>&quot;I want less mass-produced surveillance bullshit and more Harry Potter magic.&quot;</p> </blockquote> <p>I was super excited for Suz's talk, as I've followed her online for some time and I think her hardware projects are amazing, so it was great to see some of them demoed. She demonstrated how we can use web APIs to interact with non-standard interfaces like printers and other hardware.</p> <figure><picture><source type="image/webp" srcset="https://localghost.dev/blog/ffconf-2019-future-friends-beside-the-seaside/hWsYbJwrls-280.webp 280w, https://localghost.dev/blog/ffconf-2019-future-friends-beside-the-seaside/hWsYbJwrls-400.webp 400w" sizes="auto"><img loading="lazy" decoding="async" src="https://localghost.dev/blog/ffconf-2019-future-friends-beside-the-seaside/hWsYbJwrls-280.jpeg" alt="" width="400" height="267" srcset="https://localghost.dev/blog/ffconf-2019-future-friends-beside-the-seaside/hWsYbJwrls-280.jpeg 280w, https://localghost.dev/blog/ffconf-2019-future-friends-beside-the-seaside/hWsYbJwrls-400.jpeg 400w" sizes="auto"></picture><figcaption>Suz Hinton (photo: Trys Mudford)</figcaption></figure> <p>Using a thermal receipt printer, the Web Media API and the Web USB API, Suz recreated her very own Gameboy Printer! She also showed off her web app that lets you upload Arduino programs directly to the Arduino via the browser, which made me think I should dig out the Arduino I've had in a drawer for several years and never used. (We've all got a drawer like that.)</p> <figure><picture><source type="image/webp" srcset="https://localghost.dev/img/qzq_fkV8P4-280.webp 280w, https://localghost.dev/img/qzq_fkV8P4-640.webp 640w, https://localghost.dev/img/qzq_fkV8P4-960.webp 960w" sizes="auto"><img loading="lazy" decoding="async" src="https://localghost.dev/img/qzq_fkV8P4-280.jpeg" alt="Photo of Suz Hinton giving a conference talk" width="960" height="739" srcset="https://localghost.dev/img/qzq_fkV8P4-280.jpeg 280w, https://localghost.dev/img/qzq_fkV8P4-640.jpeg 640w, https://localghost.dev/img/qzq_fkV8P4-960.jpeg 960w" sizes="auto"></picture></figure> <p>The tone of the talk shifted considerably when Suz started talking about ownership over computers and data. She cited a 1985 ruling by the Federal Communications Commission which allocated the 900MHz frequency band to industrial, scientific and medical uses. Amateur radio users were allowed to use the band if they liked, as long as it didn't interfere with the protected uses.</p> <p>However, now the FCC have proposed that private companies should be able to use these bands. And guess who's jumped straight in there? Everyone's favourite Big Bad Retailer, Amazon.</p> <p>Amazon's new <a href="https://techcrunch.com/2019/09/25/amazon-sidewalk-is-a-new-long-range-wireless-network-for-your-stuff/">Sidewalk</a> network functions on this 900MHz band. Even the 700-odd Amazon employees testing the products managed to form a localised network on the 900MHz frequency stretching across much of the Los Angeles Basin. Meaning they've basically made a frequency jammer. (Bear in mind frequency jammers are illegal in many countries.) If hundreds of thousands of Amazon customers start buying these products which operate on this frequency, what's going to happen to all those medical devices that rely on this band?</p> <blockquote> <p>&quot;This is the scariest thing I've ever heard.&quot;</p> </blockquote> <p>At this point Suz started crying, which really drove home just how doomed we'll be if this stuff keeps happening. &quot;We are pretending that these devices are supposed to be helping people, and these people have no idea that they are actually contributing to a privately owned network that companies like this can do whatever they want with.&quot;</p> <p>It's our job to make sure that we are fighting against things like this - &quot;we've done a bad job at educating people&quot;. We should be building more &quot;Harry Potter magic&quot; instead of buying corporate home technology, Suz argued - she namechecked <a href="http://samanthagoldste.in/">Samantha Goldstein</a>, who built a stained glass panel that goes cloudy if it's raining.</p> <p>ffconf talk page: <a href="https://ffconf.org/talks/nerdverse/">Adventures in reinventing interfaces</a></p> <h2 id="reflections" tabindex="-1">Reflections</h2> <p>I came away from ffconf inspired and filled with a desire to build weird stuff: I want to learn to do things with hardware, make pretty patterns with CSS, and teach my niece how to do the same just for the hell of it.</p> <p>I also came away feeling terrified by corporate surveillance and the absolute disregard for ethics in technology under the guise of convenience.</p> <p>Being a developer isn't just about building flashy stuff and sending it off into the world and moving onto the next thing: we've got a responsibility to make sure that the tech we're building is doing good, not harm, and that people are educated about the implications if we do start building creepy stuff like Sidewalk. As Laura Kalbag put it: <strong>be the advocate, and question the default</strong>. We know what these products and these companies are doing, so it's our job to tell everyone about it.</p> <p>ffconf is a small, family-run conference, with passionate speakers who have a story to tell. I'd love to see more community-focused conferences like this - it's all too common nowadays to go to a big conference and end up listening to sales pitch after sales pitch when all you want to do is learn something.</p> <p>I feel like I should probably get rid of the Google Home that's sitting in my living room right now.</p> Everything I googled in a week as a professional software engineer 2019-09-02T00:00:00Z https://localghost.dev/blog/everything-i-googled-in-a-week-as-a-professional-software-engineer/ <p><strong>Update 15/10/2022:</strong> I've written a new version of this! <a href="https://localghost.dev/blog/everything-i-googled-in-a-week-as-a-senior-software-engineer/">Everything I googled in a week as a senior software engineer</a></p> <p>In an attempt to dispel the idea that if you have to google stuff you're not a proper engineer, this is a list of nearly everything I googled in a week at work, where I'm a software engineer with several years' experience.</p> <figure><picture><source type="image/webp" srcset="https://localghost.dev/img/iHnMvBCQJb-280.webp 280w, https://localghost.dev/img/iHnMvBCQJb-640.webp 640w, https://localghost.dev/img/iHnMvBCQJb-810.webp 810w" sizes="auto"><img loading="lazy" decoding="async" src="https://localghost.dev/img/iHnMvBCQJb-280.jpeg" alt="A google search for &quot;threw it on the ground&quot;" width="810" height="88" srcset="https://localghost.dev/img/iHnMvBCQJb-280.jpeg 280w, https://localghost.dev/img/iHnMvBCQJb-640.jpeg 640w, https://localghost.dev/img/iHnMvBCQJb-810.jpeg 810w" sizes="auto"></picture><figcaption>Happy birthday to the ground.</figcaption></figure> <p>Obviously these weren't all googled in a row (although you can probably spot that a few were), but throughout the day. I can't remember the context of everything I was googling, but hopefully it'll make you feel a little better next time you have to google something.</p> <h2 id="monday" tabindex="-1">Monday</h2> <p><code>npm react-testing-library</code> - during a React upgrade, looking at dependencies to see latest versions and checking for breaking changes.</p> <p><code>Expecting a parsed GraphQL document. Perhaps you need to wrap the query string in a &quot;gql&quot; tag?</code> - said React upgrade then started causing some super fun errors.</p> <p><code>react-apollo release notes</code></p> <p><code>react-apollo/test-utils</code> - tests were throwing some odd errors with our graphQL components.</p> <p><code>undo a rebase</code> - oops.</p> <p><code>react testing library apollo &quot;invariant violation&quot;</code> - package upgrades are so much fun!</p> <p><code>jest silence warnings</code> - don't judge me, ok?</p> <p><code>semantic HTML contact details</code> - wanted to check if the <code>&lt;address&gt;</code> tag was relevant here</p> <p><code>aa contrast checker</code></p> <p><code>temporary visual impairment</code> - fact checking for an accessibility talk I was giving that week</p> <p><code>dominos accessibility</code> - popcorn.gif</p> <p><code>shame gif</code> - an important part of any presentation</p> <h2 id="tuesday" tabindex="-1">Tuesday</h2> <p><code>javascript get array of unique dates</code> - if I have an array of <code>Date</code>s, how can I filter them so they are unique? (<code>reduce</code>, naturally, but I can rarely use that without googling it first)</p> <p><code>date to locale string</code></p> <p><code>js date to locale string</code> - after I got a load of Java results</p> <p><code>alternatives to Moment.js</code> - it's large</p> <p><code>group array items by date</code> - more <code>reduce</code> fun</p> <p><code>sort object keys javascript</code></p> <p><code>react fragment keys</code></p> <p><code>next link</code> - needed a reminder of how to use the Link component in Next.JS</p> <p><code>React.Children.only expected to receive a single React element child.</code></p> <p><code>visual studio code disable autocomplete html</code> - it keeps autoclosing HTML elements on the same line, and I still can't switch it off</p> <p><code>dt dd dl</code> - couldn't remember what the example use for these was.</p> <p><code>html nested sections</code> - is it ok to have <code>&lt;section&gt;</code> inside <code>&lt;section&gt;</code>?</p> <p><code>display dl in pairs</code></p> <p><code>veggie ipsum</code> - the best lorem ipsum generator</p> <p><code>css keyframes</code></p> <p><code>css animate underline text</code></p> <p><code>dl vs ul</code></p> <p><code>react generating keys</code> - should I use some kind of hash, or should I use data in the props? (I ended up constructing a string with unique timestamp data)</p> <p><code>css checkbox</code> - can we style checkboxes yet? (no)</p> <p><code>flexbox center span</code> - it was 17:24 and I was tired by this point</p> <p><code>grid minmax</code></p> <p><code>flexible grid row</code> - I don't have a whole lot of CSS Grid experience, so I always end up googling a ton with this.</p> <p><code>grid row height auto</code></p> <p><code>cauliflower shortage</code> - someone told me about this and I panicked</p> <p><code>next.js hooks</code> - we can use them, right? (we can, and I did)</p> <h2 id="wednesday" tabindex="-1">Wednesday</h2> <p><code>cors</code> - today is going to be bleak</p> <p><code>the corrs</code> - once I hit some CORS errors I decided I needed to make a meme, and I needed to find the perfect image. It took a surprisingly long time.</p> <figure><picture><source type="image/webp" srcset="https://localghost.dev/img/fU5aiNpVTM-280.webp 280w, https://localghost.dev/img/fU5aiNpVTM-640.webp 640w, https://localghost.dev/img/fU5aiNpVTM-960.webp 960w" sizes="auto"><img loading="lazy" decoding="async" src="https://localghost.dev/img/fU5aiNpVTM-280.jpeg" alt="Many many Google search results for The Corrs" width="960" height="688" srcset="https://localghost.dev/img/fU5aiNpVTM-280.jpeg 280w, https://localghost.dev/img/fU5aiNpVTM-640.jpeg 640w, https://localghost.dev/img/fU5aiNpVTM-960.jpeg 960w" sizes="auto"></picture></figure> <figure><picture><source type="image/webp" srcset="https://localghost.dev/img/yfWlXLFAs9-280.webp 280w, https://localghost.dev/img/yfWlXLFAs9-500.webp 500w" sizes="auto"><img loading="lazy" decoding="async" src="https://localghost.dev/img/yfWlXLFAs9-280.jpeg" alt="A photoshopped version of the Corrs' album with the title &quot;No Access-Control-Allow-Origin header is present on the requested resource&quot;" width="500" height="500" srcset="https://localghost.dev/img/yfWlXLFAs9-280.jpeg 280w, https://localghost.dev/img/yfWlXLFAs9-500.jpeg 500w" sizes="auto"></picture><figcaption>Worth it.</figcaption></figure> <p><code>git patch trailing whitespace</code> - I was sent a git patch with some whitespace that prevented it from actually patching</p> <p><code>jsx annotation</code></p> <p><code>web api fetch preflight</code> - in my CORS adventures I wanted to read up a bit more about preflight requests.</p> <p><code>web api fetch origin header</code></p> <p><code>discriminated union flow</code> - trying to diagnose problems with my Flow types.</p> <p><code>whitespace regex</code> - is it <code>\w</code>? (no, that's a word - it's <code>\s</code>)</p> <p><code>regex not letter</code></p> <p><code>pat butcher emoji</code> - what can I say, I google important things</p> <p><code>woman shouting at cat</code></p> <p><code>google oauth</code></p> <p><code>next.js authentication</code> - sometimes it's helpful to google stuff to see if anyone has written examples of how to do common flows in the framework or tool that you're using</p> <p><code>component displayname</code> - do I need to do this with my higher-order components?</p> <p><code>nextCookie</code> - starting to mess around with oauth cookies</p> <p><code>reading cookies in react</code> - there must be a better way than <code>document.cookie</code></p> <p><code>js-cookie npm</code></p> <p><code>cookies-js</code></p> <p><code>npm cookie</code></p> <p><code>universal-cookie</code></p> <p><code>google oauth cookie</code></p> <p>🍪</p> <h2 id="thursday" tabindex="-1">Thursday</h2> <p><code>&quot;log in with google&quot; localhost</code> - was having all sorts of problems getting this to work</p> <p><code>httpserverrequest javascript</code> - I have a feeling this was something to do with Flow types</p> <p><code>nextjs flowtypes</code> - yep, there you go</p> <p><code>&quot;python-social-auth&quot; react</code> - trying to figure out if the django backend I was working with would play nicely with my React frontend</p> <p><code>google social login</code></p> <p><code>vary header</code></p> <p><code>get cookie from 302</code></p> <p><code>google social login cookies</code> - I was having a really fun time with this as you can tell</p> <p><code>google oauth cookie</code></p> <p><code>python-social-auth set-cookie</code></p> <p><code>python-social-auth site:stackoverflow.com</code></p> <p><code>python-social-auth react site:stackoverflow.com</code></p> <p><code>django</code> - I think I gave up at this point and just googled Django because I have literally never used it</p> <p><code>fetch send cookies</code></p> <p><code>testing same origin credentials locally</code></p> <p><code>cross origin cookies</code> - spoiler alert: not a thing</p> <p><code>useState default value</code></p> <p><code>&quot;The value of the 'Access-Control-Allow-Origin' header in the response must not be the wildcard '*' when when the request's credentials mode is 'include'.</code> - googling error messages is only second to console.log() as a debugging method</p> <p><code>useState with empty array</code></p> <p><code>react hooks initial state empty array</code></p> <p><code>&quot;Provisional headers are shown&quot;</code> - this is where the requests weren't going through and I couldn't see what the actual headers being sent were</p> <p><code>fetch send cookies</code></p> <p><code>how to see request headers in chrome</code></p> <p><code>fetch not sending cookies</code></p> <p>Thursday was a whole lot of fun D:</p> <h2 id="friday" tabindex="-1">Friday</h2> <p><code>provisional headers are shown</code> - still at it.</p> <p><code>sending cookies from localhost</code> - have I mentioned that I hate cookies?</p> <p><code>editing host file</code> - desperate times (and it didn't even work)</p> <p><code>sending cookies with different domain</code></p> <p><code>next cookie</code></p> <p><code>getinitialprops functional component</code></p> <p><code>getting cookies from document</code></p> <p><code>js find</code> - to check the usage and return types</p> <p><code>string contains</code></p> <p><code>string methods js</code> - I can't keep any of these in my head</p> <p><code>js string methods</code> - 20 mins later</p> <p><code>js fetch manual cookie</code></p> <p><code>django react cookies localhost</code></p> <p><code>django react cookies localhost site:stackoverflow.com</code></p> <p><code>httponly cookie</code></p> <p><code>django httponly</code></p> <p><code>async await promise.all</code></p> <p><code>nextjs port</code></p> <p><code>google appengine node ports</code></p> <p><code>next rename static</code></p> <p><code>install gcloud cli</code></p> <p><code>method patch</code> - couldn't remember what the HTTP method <code>PATCH</code> does.</p> <p><code>nextjs env</code></p> <p><code>next.js environment variables</code></p> <p><code>next js docs</code></p> <p><code>editing data with hooks</code> - literally no idea what I was trying to google here but this was past 5pm so I was evidently quite tired</p> <p><code>react form submit</code></p> <p><code>dayjs</code> - I needed the documentation again.</p> <p>What I'm trying to show with all this is that you can do something 100 times but still not remember how to do it off the top of your head. Never be ashamed of googling, even if it seems like the most basic thing you're looking up. I can never remember how <code>Date</code> works.<br> I've built plenty of forms in React but couldn't remember how <code>onSubmit</code> worked on the Friday evening at 5:30pm. I constantly have to google JS string methods. Cookies are terrible. (Incidentally, we fixed the cookie issue by running everything in a docker container and tunneling with <code>ngrok</code>, so everything's on the same domain.)</p> Questions to ask at an engineering interview 2019-08-22T00:00:00Z https://localghost.dev/blog/questions-to-ask-at-an-engineering-interview/ <figure><picture><source type="image/webp" srcset="https://localghost.dev/img/LMK6-iG_v--280.webp 280w, https://localghost.dev/img/LMK6-iG_v--640.webp 640w, https://localghost.dev/img/LMK6-iG_v--960.webp 960w" sizes="auto"><img loading="lazy" decoding="async" src="https://localghost.dev/img/LMK6-iG_v--280.jpeg" alt="A still from the HBO series Silicon Valley in which two interviewers are asking a candidate 'it says here on your resume that from 2010 to 2011, you crushed it?'" width="960" height="890" srcset="https://localghost.dev/img/LMK6-iG_v--280.jpeg 280w, https://localghost.dev/img/LMK6-iG_v--640.jpeg 640w, https://localghost.dev/img/LMK6-iG_v--960.jpeg 960w" sizes="auto"></picture></figure> <p>Tech job interviews are often different flavours of the same thing, regardless of where you apply. Interviewers are likely to ask you questions about your experiences, perhaps a hypothetical question about what you might do in a certain situation, or delve into some of the how-it-works stuff under the hood of whatever programming language you'll be using. However, there's one question that you can <em>guarantee</em> will come up:</p> <blockquote> <p>&quot;Do you have any questions for us?&quot;</p> </blockquote> <p>(At this point you'll probably sigh with relief - internally, at least - as you've reached the end of the interview.)</p> <p>It might be tempting to just skip over this with a &quot;no, thank you&quot; - maybe the job description gave you enough information for now - but you should always ask <strong>at least one question</strong>. It's a surprisingly important part of the interview. Firstly, it's an opportunity for you to squash any worries you might have, or find out the answers to anything that wasn't quite clear before. Secondly, it's a chance for you to peek into how things <em>really</em> are at Company X before you get too far in and realise you've Made A Huge Mistake. An interview should be as much you interviewing <em>them</em> as it is them interviewing you, as you need to find out whether the company is a good fit for <em>you</em>.</p> <p>Of course, you don't have to wait until the very end of the interview to ask questions - it's great to ask relevant questions throughout (though be careful that you don't end up answering every question with another question)!</p> <h2 id="what-your-questions-say-about-you" tabindex="-1">What your questions say about you</h2> <p>This part of the interview is not just for your benefit: the questions you ask tell the interviewer more about you than you might think. For starters, it shows how engaged you are, what your priorities are and the kind of things you value in your role. It's an opportunity to show off your knowledge and experience by asking questions tailored to the work that you're looking for.</p> <p>How many questions is too many? It depends how much time you've got left, but don't be afraid to turn up with a list. I brought a notebook into mine, and I took notes while they answered.</p> <p>So, what kind of questions might you ask? Obviously it's a chance for you to get answers to things that weren't clear about the application process or the job itself. You can ask about the kind of learning and development opportunities, or ask the old favourite of &quot;what do you like most about working here&quot;? Don't be afraid to spin questions that the interviewer asked you back around on them, as long as it's relevant and reasonable.</p> <p>My favourite interview question tactic is to think about things from my previous roles that frustrate you or that you know you don't want to experience again. Asking questions about these things can give you an idea of whether they do things differently at this place, or whether you're walking into the same job all over again. (Alternatively, if there's something you particuarly enjoy about your current role, it's a good way of finding out whether it'll be the same at this new place too.)</p> <p>I'll give you some questions that I might ask as a starter (and in some cases, what these questions are <em>really</em> asking). I'm not saying you should ask all of these questions - that would probably take all afternoon - but feel free to pick and choose depending on where you're applying and what's important to you. They're here as a starting point.</p> <h2 id="the-how-does-this-work-here-questions" tabindex="-1">The &quot;how does this work here?&quot; questions</h2> <p>These are the questions based on your past experience. It's a great way to understand more about processes at the company, as well as making sure you won't end up working at somewhere that's exactly the same as your last place (unless that's something you want).</p> <h3 id="working-in-product-teams" tabindex="-1">Working in product teams</h3> <blockquote> <p>How closely do the dev team work with business stakeholders/customers?</p> </blockquote> <p>Is this a case of &quot;IT vs The Business&quot; or are engineers embedded throughout the company? I've worked in both situations, and it's much nicer being considered a core part of the business rather than a &quot;supporting function&quot; building a product for someone else's business needs. Is there a product manager on the team who represents the needs of the customer? Are there regular demos with relevant stakeholders? Do they do any user testing?</p> <blockquote> <p>How does work come in to the team?</p> <p>How involved are engineers with deciding what to build?</p> </blockquote> <p>Am I going to be involved in the decision making and the shaping of what we're building or am I going to be blindly picking up JIRA tickets? Is it an iterative development process, or is it a case of &quot;requirements come in, code comes out 6 months later&quot;?</p> <p>It took a bit of getting used to, but now I love being involved right at the beginning, helping to decide what it is we're going to build and how we're going to build it. It's related to the first question in that it shows that engineering is a core part of the company and not just something that you do to achieve the thing for the business.</p> <blockquote> <p>Who makes technical decisions? How much independence do product teams have to make technical decisions?</p> </blockquote> <p>This question is about autonomy in what you're building. Is it going to be a case of &quot;here is the work, go and do it&quot; or &quot;how shall we build this? What do you think?&quot;</p> <p>Some degree of uniformity is vital, especially if you're working with lots of microservices. But having strategies for the way you work and the technologies you use is one thing; being told exactly what to do and having no independence or flexibility is another thing entirely.</p> <h3 id="code-deployment-and-infrastructure" tabindex="-1">Code, deployment and infrastructure</h3> <blockquote> <p>Who looks after the live applications?</p> </blockquote> <p>Is it &quot;you build it, you run it&quot; or is it &quot;you build it, and throw it over the wall&quot;? Do engineers have ownership of the apps they build, and are they trusted to look after them?</p> <p>It's also worth finding out how on-call works, because if that's something that you can accommodate, it's a great opportunity to learn about the tech and how things work.</p> <blockquote> <p>What's your deployment process like?</p> <p>What does an average path to production look like for a service or application?</p> <p>Do you practice continuous integration?</p> <p>How often do you release to production?</p> </blockquote> <p>Can I deploy stuff myself on an ad-hoc basis, or is it once a week/every three months? Is it going to be six months before you let me put it into the wild?</p> <p>This is about finding out how much friction there is in the path to production. The more obstacles and Process With A Capital P, the more obstacles to innovation and experimentation. <em>That said</em>, there is a happy medium, and I don't believe you should be firing off software into prod without due diligence, some process and quality control, especially when you're working with people's data. It's worth finding out how the company handles that delicate balance.</p> <p>Bonus points for continuous deployment.</p> <blockquote> <p>Say I wanted to spin up a new service. How would I do that?</p> </blockquote> <p>Would I need to get permission from the Change Advisory Board in a distant ivory tower? Would I need to write numerous documents and have handover meetings for deployment and maintenance?</p> <p>The answer I got in my Monzo interview was &quot;there's a command line script to generate a new service&quot; and I nearly wept with joy.</p> <blockquote> <p>How do you do version control?</p> </blockquote> <p>If they say &quot;Subversion&quot;, it's time to question why. Is there a big beast of a monolith that was written in Java 6 and nobody has quite had the guts to move it off SVN because they aren't <em>quite</em> sure where all the code lives, and do you really want to work with that every day?</p> <blockquote> <p>How are architectural decisions made?</p> <p>Do you have an architecture team? What do they do?</p> </blockquote> <p>Do you have people whose job title is literally &quot;Architect&quot;? If so, are they hands-on and technical, or do they come in and tell you how to build stuff and then leave again? Am I going to be allowed to make my own decisions about the structure of the software I'm building?</p> <p>I've definitely met some good architects, but really things like solution architecture should be done by the team building the actual thing.</p> <blockquote> <p>What's your testing strategy?</p> </blockquote> <p>Do your developers do any testing? (Conversely, are you obsessed with as high test coverage as possible?) And if there's pretty poor test coverage, are they open to improving it or do they see it as a waste of time?</p> <h2 id="the-how-is-it-really-questions" tabindex="-1">The &quot;how is it really?&quot; questions</h2> <p>The questions that help you delve into day-to-day life at the company.</p> <blockquote> <p>What does a typical day look like for you?</p> <p>What are the working hours like here?</p> </blockquote> <p>Will I be working until 8pm, or can I go to my clay pigeon shooting evening class without getting Meaningful Glances from my colleagues behind their screens?</p> <p>Perhaps I'm naïve, but I firmly believe in a work-life balance. I was very lucky to have that in my previous job, and I have it in my current job. I don't believe working later makes you a better contributor, as I think the more tired and burned out you get the worse your work is going to be in the long run. I want to know what kind of hours people keep so I can factor it into my decision.</p> <blockquote> <p>Are there many parents here?</p> <p>How does parental leave work here?</p> <p>What facilities do you have for new parents?</p> </blockquote> <p>This one is not a question I've asked, as I don't have children and don't plan to have them any time soon, but if you're a parent or at that stage in your life where it's something you're thinking about, you want to know whether it's a parent-friendly workplace, whether there's anywhere to go and breastfeed, and what kind of caregiver leave they offer.</p> <p>I know that some places can be a bit funny if women of childbearing age ask about this kind of thing in interviews; if that's the case, I would question whether it's really somewhere you want to work.</p> <blockquote> <p>What kind of diversity and inclusion initiatives do you have here?</p> </blockquote> <p>Does the company take diversity and inclusion seriously? Many (if not most) tech companies have traditionally very poor diversity in terms of gender and ethnicity - and often age, too. Trying to hire from a more diverse pool of candidates is one thing, but how are they trying to make things better in order to get people to stay? Are they hiring a diversity and inclusion lead? Do they have groups or networks?</p> <h2 id="the-where-can-i-go-from-here-questions" tabindex="-1">The &quot;where can I go from here?&quot; questions</h2> <blockquote> <p>What kind of learning and development opportunities are there for employees?</p> </blockquote> <p>A good way to find out whether they have a learning budget, or whether you can ask for books/courses/conference tickets.</p> <blockquote> <p>Do you ever host meetups or community events?</p> </blockquote> <p>Perhas not important to everyone, but as someone who owes a lot of my professional development to the community as a whole, it's really important that wherever I work has a good community spirit.</p> <blockquote> <p>Do people here ever speak at conferences?</p> </blockquote> <p>This one is a bit less helpful as (especially if it's a small team) it could be that the employees don't <em>want</em> to speak at a conference, and that's totally fine. But if it's a huge company and nobody really speaks at conferences, it kind of raises the question of whether anyone does anything worth talking about (or, if they are, whether the company isn't keen on letting them talk about it.)</p> <hr> <p>I hope this has given you an idea of the kind of questions you might want to ask at an interview, but more importantly, the value you can get from asking them. If you've got any more thoughts or ideas for interview questions, share this on Twitter with the link below and join the conversation!</p> Why software engineers should know their audience 2019-04-06T00:00:00Z https://localghost.dev/blog/why-software-engineers-should-know-their-audience/ <p>When you're talking about what you're working on, do you ever stop to think about what you're saying and whether the person you're talking to can actually understand it?</p> <p>There's more to being a software engineer than coding. What people often underestimate are the so-called &quot;soft skills&quot; (the worst term for something that's really vital) - the way that you interact with others around you. In particular, how you communicate what you're doing to people with different skillsets.</p> <p>Imagine you're in a standup and someone gives this update:</p> <blockquote> <p>I'll synthesize the haptic DDR3 network, that should shut down the SDD capacitor!</p> </blockquote> <p>This came straight from the brilliant <a href="http://shinytoylabs.com/jargon/">Hollywood Jargon Generator</a>, but to anyone who doesn't have as much technical experience as you, that might as well have been what you just said.</p> <h2 id="u-wot-m8" tabindex="-1">u wot m8</h2> <p>One of the best pieces of advice I ever got was from a tech lead who took all of the engineers aside after a standup where we'd been discussing how we were getting on with the new authentication flow we were writing. She told us to make sure we didn't go into too much technical detail in the standup, so that everyone could understand it. Sounds so simple, right?</p> <p>We'd just spent ten minutes with the rest of the team - including our product manager and the marketing lead - and we'd been talking about refresh tokens and token expiry. It had meant absolutely nothing to several of the people there. The more we mentioned these arcane technical concepts, the less the rest of the team wanted to come along to standups because they felt it was irrelevant or above them in some way.</p> <p>&quot;But it's a standup!&quot; I hear you cry. &quot;You're only meant to give a quick high-level update anyway!&quot;</p> <p>You'd be surprised how easy it is to start talking in jargon without even realising for even the smallest of updates. Next time you give a standup update, ask yourself: if I weren't technical, would I know what I was talking about?</p> <figure><picture><source type="image/webp" srcset="https://localghost.dev/img/LQYchvVt9l-280.webp 280w, https://localghost.dev/img/LQYchvVt9l-461.webp 461w" sizes="auto"><img loading="lazy" decoding="async" src="https://localghost.dev/img/LQYchvVt9l-280.jpeg" alt="&quot;Two characters are talking. Left: What've you been up to? Right: Doing tons of math for my thesis. Left: Can you explain it like I'm five? Right: Oh my God, where are your parents?&quot;" width="461" height="711" srcset="https://localghost.dev/img/LQYchvVt9l-280.jpeg 280w, https://localghost.dev/img/LQYchvVt9l-461.jpeg 461w" sizes="auto"></picture><figcaption>source: xkcd</figcaption></figure> <p>How much technical detail do you really need to go into? If your standup tends to be &quot;here's what I did yesterday and here's what I'm doing today&quot;, do you need to say:</p> <blockquote> <p>We're still trying to get the auth service to store the user's refresh token in the database and check it against the incoming requests.</p> </blockquote> <p>or can you just say</p> <blockquote> <p>We're still trying to get authentication working in the backend.</p> </blockquote> <p>To you the first example might look pretty easy to understand, but that's probably because you're an engineer. You know what a refresh token is. You know what a request is in this context. Non-technical folk probably knows what a database is, and what authentication is, but that's about it, and they've switched off by the end of the sentence. By keeping the technical detail to a minimum you'll be providing an update that the rest of the team understand, so they know what everyone's working on.</p> <p>What about if there's a particular technical thing that's blocking you that you wanted to mention in standup? Either mention that you're generally blocked in the update and approach one of your fellow engineers afterwards, or maybe have a 5-minute dev-only standup after the main team standup where you can go into more technical detail.</p> <p>Here's another example:</p> <blockquote> <p>Some of our ElasticSearch nodes went down last night, it turns out we are still using the old cluster so we're going to migrate to the new cluster today.</p> </blockquote> <p>vs.</p> <blockquote> <p>Our search system went down last night, it turns out we're using an old server* so we're going to move it to the newer one today.</p> </blockquote> <p>* I know this isn't technically the same thing, but to someone who doesn't know what a cluster is, it literally doesn't matter. You can go into more detail in the relevant dev standup/slack channel/coffee break.</p> <h2 id="use-real-world-analogies" tabindex="-1">Use real-world analogies</h2> <p>Keeping your language simple works for technical people too. If you're mentoring another engineer or helping someone work through a problem, being able to explain technical concepts in an easy-to-understand way and relating them to tangible concepts in the real world can really help others to learn from you. You'll find it a useful skill for giving talks - there's a real difference between &quot;show me how&quot; and &quot;make me understand&quot;. For example, in a talk about <a href="https://redux-saga.js.org">redux-saga</a> I used pictures of dogs to illustrate the library of effects it provides. In pretty much every object-oriented programming language tutorial there is some kind of <code>Person</code> or <code>Car</code> class to help you understand the concept of an object, and the tutorials about composition and inheritance usually talk about different species of animals.</p> <p>With the auth token example, I told my team a story about a secret members' club involving an ever-changing secret handshake and a badge that let you learn the new secret handshake, which went down well and helped the others to get what we were trying to do, even though my product manager thought I was talking about a sex club.</p> <h2 id="yeah-so-we-really-need-to-reticulate-the-splines-in-our-codebase" tabindex="-1">Yeah, so we really need to reticulate the splines in our codebase</h2> <p>It's not just standups where this skill comes in handy: in your day-to-day role, explaining technical things clearly and simply might be the thing that convinces them why you should take a break from delivering new features to go back and tackle the mountain of tech debt that's been piling up:</p> <blockquote> <p>We need to upgrade a lot of the packages, check for security vulnerabilities and replace a lot of deprecated code.</p> </blockquote> <p>What does &quot;deprecated&quot; mean? What are packages in this context?</p> <blockquote> <p>We've got some old code that needs updating because soon it won't work any more, and it might pose a security risk.</p> </blockquote> <p>It sounds so basic, but to a non-engineer, that's all they need to know. They might ask some questions about what kind of risk it poses, or what &quot;won't work any more&quot; means, but it's a lot easier to follow.</p> <h2 id="challenge-yourself-to-keep-it-simple" tabindex="-1">Challenge yourself to keep it simple</h2> <p>It's not always easy to translate the hard stuff in easy terms, especially when you're working on things that involve kubernetes (true story). I still haven't found a good layman's explanation for what Docker does. But next time you're in a standup, listen carefully to what your fellow engineers say and then catch up with your other team members afterwards to ask if they followed everything. If they say yes, then you're fine, and you can just carry on as you were. But if they say no, consider getting your team to challenge themselves to deliver very simple standup for a a week. Make up ridiculous stories to illustrate migrating ElasticSearch clusters. At the end, ask those same people whether they feel like they know more about what you've been up to.</p> <p>And if all else fails, there's my old favourite fallback:</p> <blockquote> <p>Everything's slightly broken, but I'm working on making it not broken.</p> </blockquote>