<?xml version="1.0" encoding="utf-8"?>
<feed xmlns="http://www.w3.org/2005/Atom">
  <title>trashpanda.cc</title>
  <subtitle>Tech posts to educate, elevate, but mostly entertain.</subtitle>
  <link href="https://trashpanda.cc/rss.xml" rel="self"/>
  <link href="https://tp-11ty.onrender.com"/>
  <updated>2024-09-03T00:00:00Z</updated>
  <id>https://tp-11ty.onrender.com</id>
  <author>
    <name>Tim Duckett</name>
    <email>tim@duckett.de</email>
  </author>
  <entry>
    <title>Deploying an Eleventy site to render.com</title>
    <link href="https://tp-11ty.onrender.com/blog/2024-09-03-deploying-11ty-to-render/"/>
    <updated>2024-09-03T00:00:00Z</updated>
    <id>https://tp-11ty.onrender.com/blog/2024-09-03-deploying-11ty-to-render/</id>
    <content type="html">&lt;p&gt;&lt;em&gt;tl;dr&lt;/em&gt; If you try to deploy the &#39;hello, world!&#39; Eleventy site to Render, it will fail with a &lt;code&gt;heifsave: Unsupported compression (via Template render error)&lt;/code&gt; error. To fix this, you need to remove the &lt;code&gt;avif&lt;/code&gt; image type from &lt;code&gt;.eleventy.config.images.js&lt;/code&gt;&lt;/p&gt;
&lt;p&gt;&lt;em&gt;slightly longer; might read&lt;/em&gt; Handling AVIF image types requires HEIC libraries to be installed on the system that builds the eleventy site, and render.com doesn&#39;t have this. Hence the error, and hence the need to remove this as a valid image type in the file that controls image processing.&lt;/p&gt;
&lt;p&gt;&lt;em&gt;even longer; not strictly relevant&lt;/em&gt; While render.com is a nice brand name, it doesn&#39;t make for very good search engine optimisation because the results are inevitably polluted by references to &#39;render&#39; as in the verb &#39;to convert something&#39;.  It doesn&#39;t make finding problem resolutions any easier...k&lt;/p&gt;
</content>
  </entry>
  <entry>
    <title>How to manage a statically-generated website from iOS devices</title>
    <link href="https://tp-11ty.onrender.com/blog/2028-08-21-blogging-from-ios/"/>
    <updated>2024-08-21T00:00:00Z</updated>
    <id>https://tp-11ty.onrender.com/blog/2028-08-21-blogging-from-ios/</id>
    <content type="html">&lt;p&gt;&lt;strong&gt;tl;dr&lt;/strong&gt; with the aid of two apps, you can create and manage content for websites with git-based static site publishing workflows like &lt;a href=&quot;https://gohugo.io/&quot;&gt;Hugo&lt;/a&gt;, &lt;a href=&quot;https://www.11ty.dev/&quot;&gt;Eleventy&lt;/a&gt; and others.&lt;/p&gt;
&lt;h1&gt;Why is this worth doing?&lt;/h1&gt;
&lt;p&gt;If you&#39;ve outgrown the blog-as-a-service platforms like &lt;a href=&quot;https://wordpress.com/hosting/&quot;&gt;Wordpress&lt;/a&gt; or &lt;a href=&quot;https://www.typepad.com/&quot;&gt;Typepad&lt;/a&gt;, running your own site often involves using tools like &lt;a href=&quot;https://gohugo.io/&quot;&gt;Hugo&lt;/a&gt; or &lt;a href=&quot;https://www.11ty.dev/&quot;&gt;Eleventy&lt;/a&gt;.&lt;/p&gt;
&lt;p&gt;They&#39;re basically static site generators - create content in text files, use templates to define layouts, run the content through a processing engine, and then publish the resulting HTML and assets to a web server. They&#39;re fast, powerful, flexible and decouple the content management from the hosting.&lt;/p&gt;
&lt;p&gt;Many of this type of tools can also take advantage of git-based deployment - you configure something like Github Actions to react to pushes to the site&#39;s repository by building the content and transferring it over to the hosting platform. Some hosting services like Render will do the same thing in the opposite direction - they watch the respository for changes, and fetch and build the updates when something pushed.&lt;/p&gt;
&lt;p&gt;Regardless of what tool, source code respository and hosting platforms are involved, the content creation process is usually similar. You edit the source files in some text editor, then commit the changes using the command line or a Git client. That&#39;s straight-forward enough on a desktop system, but tends not to play nicely with mobile devices.&lt;/p&gt;
&lt;p&gt;I spend much more time using my mobile devices than I do sat in front of a Macbook, so I wanted a way of being able to CRUD content on my sites with the tools that I&#39;ve got in my hand.  The good news is that it&#39;s a) possible and b) very straight-forward, albeit at the cost of a couple of (very reasonably priced imho) apps.&lt;/p&gt;
&lt;p&gt;The process also doesn&#39;t need any changes to either the publishing workflows or the hosting platform.&lt;/p&gt;
&lt;h1&gt;How it works&lt;/h1&gt;
&lt;p&gt;For the purposes of this explanation, I&#39;ve got the following moving parts:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;a href=&quot;https://gohugo.io/&quot;&gt;Hugo&lt;/a&gt; as my static site generator&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://github.com/&quot;&gt;Github&lt;/a&gt; as the source code repository&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://render.com/&quot;&gt;Render&lt;/a&gt; as my hosting platform&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;The desktop workflow goes:&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;Create a new post in the local repo copy of the site&#39;s source code using a text editor&lt;/li&gt;
&lt;li&gt;Commit the changes to Github&lt;/li&gt;
&lt;li&gt;Render watches the repository&lt;/li&gt;
&lt;li&gt;When it detects changes, it checks out the updated source, builds the static site content, and deploys to its content distribution network automatically.&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;This process is going to reproduce that workflow, but with mobile tools:&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;A local copy of the site&#39;s source code will live on an iPad (you can subsitute iPhone for iPad as needed, because the process is exactly the same)&lt;/li&gt;
&lt;li&gt;A text editor app will be used to create and/or update the site source&lt;/li&gt;
&lt;li&gt;A Git client app will handle pushing and pulling to the Github repo&lt;/li&gt;
&lt;li&gt;All other components and workflows will remain exactly the same and unchanged.&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;For this you&#39;ll need an iOS Git client (I&#39;m going to use Working Copy) and an iOS text editor that can open and save files in Markdown format (I&#39;m going to use Textastic).  Both of these are paid apps with free trials - the total outlay will be about 35€, but to me that&#39;s a reasonable price for the flexibility of being able to manage my site on the go.&lt;/p&gt;
&lt;h1&gt;Setup&lt;/h1&gt;
&lt;p&gt;Setup is split into two parts - the Git component, and the text editor&lt;/p&gt;
&lt;h3&gt;Setting up Working Copy as the Git client&lt;/h3&gt;
&lt;ol&gt;
&lt;li&gt;Install &lt;a href=&quot;https://apps.apple.com/app/apple-store/id896694807?pt=15897&amp;amp;ct=website&amp;amp;mt=8&quot;&gt;Working Copy&lt;/a&gt; from the App Store&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://workingcopy.app/manual/cloning-repos&quot;&gt;Clone the site&#39;s respository from Github&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;Enable Working Copy as a file source &lt;a href=&quot;https://support.apple.com/en-us/102238&quot;&gt;using these instructions&lt;/a&gt;&lt;/li&gt;
&lt;/ol&gt;
&lt;h3&gt;Open the local files in Textastic&lt;/h3&gt;
&lt;ol&gt;
&lt;li&gt;Install &lt;a href=&quot;https://apps.apple.com/us/app/textastic-code-editor/id1049254261?ct=textasticapp.com&amp;amp;pt=15967&amp;amp;mt=8&quot;&gt;Textastic&lt;/a&gt; from the App Store&lt;/li&gt;
&lt;li&gt;Add the repo to the sidebar
&lt;ol&gt;
&lt;li&gt;In the Textastic sidebar, tap the &lt;code&gt;Add external folder&lt;/code&gt; option&lt;/li&gt;
&lt;li&gt;Tap on the &lt;code&gt;Working Copy&lt;/code&gt; entry in the &lt;code&gt;Locations&lt;/code&gt; section of the sidebar&lt;/li&gt;
&lt;li&gt;Open the folder for the repo&lt;/li&gt;
&lt;li&gt;Navigate down to the content files (e.g. Hugo&#39;s content files normally live in &lt;code&gt;./content/posts&lt;/code&gt; )&lt;/li&gt;
&lt;li&gt;Tap &lt;code&gt;Open&lt;/code&gt; to add the folder to the Textastic side bar&lt;/li&gt;
&lt;/ol&gt;
&lt;/li&gt;
&lt;/ol&gt;
&lt;h1&gt;Operation&lt;/h1&gt;
&lt;ol&gt;
&lt;li&gt;Create a new post file in Textastic (I&#39;ve found the easiest way to do this is long-press on an existing post file, tap the &lt;code&gt;Duplicate&lt;/code&gt; option to clone it, then edit the new file to overwrite the existing content)&lt;/li&gt;
&lt;li&gt;Switch to Working Copy&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://workingcopy.app/manual/commit-revert&quot;&gt;Create a new commit&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://workingcopy.app/manual/commit-revert&quot;&gt;Push the commit to the remote repo&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;Wait for the changes to be built and published.&lt;/li&gt;
&lt;/ol&gt;
&lt;h1&gt;Conclusion&lt;/h1&gt;
&lt;p&gt;For a total outlay of around 40€, it&#39;s possible get the best of both worlds - the utility of a mobile device that you&#39;ve always got to hand; and the power and flexibility of a modern static site generator. It feels like a step closer to a world where table and laptops are functionally-equivalent - we&#39;re not there quite yet, but being able to manage sites without being dependent on &amp;quot;monolithic&amp;quot; client apps is definitely progress.&lt;/p&gt;
</content>
  </entry>
  <entry>
    <title>Keeping email uncluttered with a &quot;waiting for&quot; folder</title>
    <link href="https://tp-11ty.onrender.com/blog/2024-08-24-waiting-for/"/>
    <updated>2024-08-21T00:00:00Z</updated>
    <id>https://tp-11ty.onrender.com/blog/2024-08-24-waiting-for/</id>
    <content type="html">&lt;p&gt;About the single easiest thing you can do to keep on top of an email inbox is &lt;em&gt;not&lt;/em&gt; to use it for long term storage.&lt;/p&gt;
&lt;p&gt;The simplistic way of achieving this is to delete every message once it&#39;s been read. But the problem with this approach is that not every message &lt;em&gt;can&lt;/em&gt; be deleted once it&#39;s been read. Suppose it&#39;s details of an order that you&#39;ll need to follow up on, or the contact details for a meeting that&#39;s going to take place next week? You&#39;ll need to keep these, at least for a while, but ideally you don&#39;t want them hanging around cluttering your inbox.&lt;/p&gt;
&lt;p&gt;The solution is very straight-forward.&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;Create a new folder called &amp;quot;waiting-for&amp;quot;, and pin it st the top of the folders list&lt;/li&gt;
&lt;li&gt;When an email comes in, read it.&lt;/li&gt;
&lt;li&gt;If it&#39;s not needed any more, delete it.&lt;/li&gt;
&lt;li&gt;If it&#39;s relevant to something that you&#39;re waiting for, drag it into the &amp;quot;waiting for&amp;quot; folder&lt;/li&gt;
&lt;li&gt;When the order arrives, or after the meeting, or whatever - delete the relevant message from the &amp;quot;waiting for&amp;quot; folder&lt;/li&gt;
&lt;li&gt;Periodically scan through the &amp;quot;waiting for&amp;quot; folder for anything that&#39;s late, and delete anything that&#39;s aged out&lt;/li&gt;
&lt;/ol&gt;
</content>
  </entry>
  <entry>
    <title>My information management process</title>
    <link href="https://tp-11ty.onrender.com/blog/2024-08-19-my-information-management/"/>
    <updated>2024-08-19T12:00:00Z</updated>
    <id>https://tp-11ty.onrender.com/blog/2024-08-19-my-information-management/</id>
    <content type="html">&lt;p&gt;Reading about other people&#39;s tools and systems is always interesting, so I figured I&#39;d sit down and record what I&#39;ve come up with.&lt;/p&gt;
&lt;p&gt;It&#39;s a process that&#39;s evolved over the years to try to capture and manage information - calling it an &lt;em&gt;&amp;quot;information management system&amp;quot;&lt;/em&gt; would be grandiose and pretentious, but this workflow is what (currently)works for me.&lt;/p&gt;
&lt;p&gt;On a daily basis I read a lot of things that &lt;em&gt;could&lt;/em&gt; be useful at some point in the future, but unless I explicity try and store them somehow, they get lost. Online search is becoming ever-less useful in the age of AI slop, and a lot of online content is only fleeting. And that&#39;s &lt;em&gt;after&lt;/em&gt; I&#39;ve allowed for my imperfect memory and recall.&lt;/p&gt;
&lt;p&gt;The process has two basic components:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;somewhere to store text-based information in a way that&#39;s as flexible as possible&lt;/li&gt;
&lt;li&gt;tags and search to (hopefully) retrieve info when needed.&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;I&#39;ve tried to end up with something as low-friction as possible, on the basis that if it&#39;s easy to use and as far as possible always within reach, actually &lt;em&gt;using it&lt;/em&gt; will become an embedded habit.  That helps when trying to capture &amp;quot;fleeting thoughts&amp;quot; - flashes of what passed for insight in the shower, that kind of thing.&lt;/p&gt;
&lt;h1&gt;Principles&lt;/h1&gt;
&lt;p&gt;From what I&#39;ve evolved, I can reverse-engineer some &amp;quot;principles&amp;quot; which makes everything sound way more calculated and sophisticated than it actually &lt;em&gt;is&lt;/em&gt;.&lt;/p&gt;
&lt;h3&gt;Don’t try to remember anything&lt;/h3&gt;
&lt;p&gt;This is something that I picked up decades ago from Dave Allen&#39;s &amp;quot;Getting Things Done&amp;quot; book. (My) human memory is imperfect, so don&#39;t rely on it. If there&#39;s a chance that a piece of information might be wanted again in the future, throw it into the system as soon as you find it, and rely on the system to surface it again. This approach works equally-well for both information and future tasks&lt;/p&gt;
&lt;h3&gt;Keep the tools at hand&lt;/h3&gt;
&lt;p&gt;If the tools aren&#39;t immediately to hand, you&#39;re back on relying on human memory in order to remember to get the info into the system. Therefore, put the tools where you can reach them - which in my case means a iPhone, an iPad and a Macbook in that order.&lt;/p&gt;
&lt;h3&gt;Keep the friction minimal&lt;/h3&gt;
&lt;p&gt;If the ingestion process is slow and painful, you&#39;ll end up not bothering. Ideally, a single click or tap should be enough. I&#39;m not quite there with my processes, but I&#39;m close enough for this to take just a few seconds in the moment - which is fast enough.&lt;/p&gt;
&lt;h3&gt;Good enough is better than perfect&lt;/h3&gt;
&lt;p&gt;There&#39;s no such thing as an ideal system, so don&#39;t bother trying to shoot for perfection - instead, aim for the sweet spot between &amp;quot;minimal friction&amp;quot; and &amp;quot;workable solution&amp;quot;. That helps to resist the temptation to replace &lt;em&gt;using&lt;/em&gt; the system with &lt;em&gt;tweaking&lt;/em&gt; the system.&lt;/p&gt;
&lt;h3&gt;Allow it to evolve...&lt;/h3&gt;
&lt;p&gt;See above - the available tooling evolves over time, and so do the circumstances your&#39;re operating in. The occasional tweak is needed to keep the processes relevant, so give yourself permission to do that.&lt;/p&gt;
&lt;h3&gt;...but try to stick with some consistency&lt;/h3&gt;
&lt;p&gt;The flip side of evolution is constant tinkering, and ideally you want a system that is familiar enough to operate with muscle memory (see the point about keeping the friction minimal). Chopping and changing tooling every 10 minutes will get in the way of that, so resiste the new shiny for shiny&#39;s sake as much as possible.&lt;/p&gt;
&lt;h1&gt;Where the information comes from&lt;/h1&gt;
&lt;p&gt;With some principles as a background, where does the information actually come from? In my case, it&#39;s overwhelmingly digital - in rough descending order of frequency, the sources are:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;web&lt;/li&gt;
&lt;li&gt;email newsletters (which tend to resolve to the web)&lt;/li&gt;
&lt;li&gt;RSS feeds (which are shortcuts to the web)&lt;/li&gt;
&lt;li&gt;Posts on services like Mastodon and LinkedIn (which are often shortcuts as well)&lt;/li&gt;
&lt;li&gt;Kindle books&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;The emphasis on web-based sources that means that something web-centric is going to cover at least 80% of my needs. That has two important benefits - the solution is roughly one-size-fits-all, and I&#39;ve no need for more complex workflows like forwarding email and so on.&lt;/p&gt;
&lt;h1&gt;Tools&lt;/h1&gt;
&lt;p&gt;These are the building blocks:&lt;/p&gt;
&lt;h3&gt;Text notes&lt;/h3&gt;
&lt;p&gt;I create text notes using the Bear app. This checks off multiple requirements:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;the content is text-based and stored in Markdown&lt;/li&gt;
&lt;li&gt;Bear is relatively open in terms of format, so I can get the information in and out easily&lt;/li&gt;
&lt;li&gt;it&#39;s cross-platform, in the sense that  it works on phone, iPad and Mac&lt;/li&gt;
&lt;li&gt;it&#39;s exceptionally well-designed, so actually &lt;em&gt;using&lt;/em&gt; it is pleasurable&lt;/li&gt;
&lt;li&gt;it plays nicely with the platform, with first-class support for share sheets and x-url linking&lt;/li&gt;
&lt;/ul&gt;
&lt;h3&gt;Handling web content&lt;/h3&gt;
&lt;p&gt;I use Omnivore for summarising long-form web content. This leverages four Omnivore features - the one-click &amp;quot;save for later&amp;quot;, the format-stripping that converts the page into text-only form,  highlighting of chunks within the text, and the ability to export those highlights back out to Bear.&lt;/p&gt;
&lt;p&gt;I don&#39;t use Omnivore as a bookmarks manager, although it could do this. Once I&#39;ve read a page, it gest archived. Bookmarks get stored as notes within Bear, and I&#39;ve got an item on my to-do list to figure out what better/more flexible approach could be. For now, though, it&#39;s good enough.&lt;/p&gt;
&lt;h3&gt;Handling RSS feeds&lt;/h3&gt;
&lt;p&gt;I use RSS feeds purely as a way of scanning through a large number of sites very rapidly. NetNewsWire runs on all three devices, and I open the posts that look interesting in Safari before using Bear or Omnivore to deal with them.&lt;/p&gt;
&lt;h3&gt;Kindle&lt;/h3&gt;
&lt;p&gt;Ebooks are a mixed blessing - the idea of being able to carry around an effectively infinite number of books on a device weighing grams is the future already being here; but Kindles also facilitates building a backlog of hundreds of books because it&#39;s just too easy to click on another one.&lt;/p&gt;
&lt;p&gt;I&#39;m also not super-happy about being locked into a specific vendor ecosystem, and some parts of the experience are clunky. But a Kindle is good enough, even if deep-down I&#39;d prefer the tactile experience of dragging a highlighter across a paper page.&lt;/p&gt;
&lt;h1&gt;Dealing with content&lt;/h1&gt;
&lt;p&gt;My basic process is:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;skim the content in whatever inbox it&#39;s landed&lt;/li&gt;
&lt;li&gt;open it in Safari if it looks interesting&lt;/li&gt;
&lt;li&gt;using the Safari share sheet, add a note into Bear&lt;/li&gt;
&lt;li&gt;stick hashtags in the note before saving&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;If the content is long-form and looks worth reading in more detail, I&#39;ll save it into Omnivore.  Periodically I&#39;ll trawl through Omnivore to see what&#39;s there, and use the highlighting function to mark the intersesting parts. Then the &amp;quot;export highlights&amp;quot; function creates a blob of text that can be pushed into Bear via the share sheet, together with a link to the original and any hashtags that I add.&lt;/p&gt;
&lt;p&gt;A combination of hashtags and search within Bear is how I find things again. I&#39;m pretty liberal with tagging, on the basis that I&#39;m trying to aid keyword searches rather than build an alternative to the Dewey Decimal system.&lt;/p&gt;
&lt;p&gt;I do use a few specific tags, though - &lt;code&gt;#ideas&lt;/code&gt; and &lt;code&gt;#sparkfile&lt;/code&gt; for &amp;quot;interesting things that might turn into a future side project if only I had the time&amp;quot;, and specific tags for things that I&#39;ve got going on at the moment (&lt;code&gt;#moving&lt;/code&gt; is an example for collecting everything relating to a pending house move).&lt;/p&gt;
&lt;p&gt;If there&#39;s something specific that I want to deal with and it&#39;s important enough to become an item on a to-do list, then I&#39;ll use the share sheet within Bear to create a to-do item in the built-in iOS reminders app. That comes along with an x-url link to the specific note within Bear.&lt;/p&gt;
&lt;h1&gt;Some of the things I don’t do&lt;/h1&gt;
&lt;p&gt;There are a few things that I just don&#39;t bother with.&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;p&gt;&lt;strong&gt;any kind of AI&lt;/strong&gt;  Summarisation is supposed to be one of AI&#39;s killer features, but that feels largely pointless. The process of scanning through an article and making highlights of the important bits is part of the comprehension process to me, so outsourcing that wouldn&#39;t win me back any time.&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;strong&gt;using dicatation or voice notes to create content&lt;/strong&gt; I&#39;d feel a bit self-conscious doing that in the firstplace, and I&#39;d need to be some kind of voice-to-text service in the loop. Maybe it would save time, but it&#39;s not something I&#39;ve bothered with (yet).&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;strong&gt;using an external bookmarking service&lt;/strong&gt; I have done in the past, but it feels a bit like opening myself up to ransom if I end up relying on someone else&#39;s goodwill or product roadmap.&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;strong&gt;expect this process to last&lt;/strong&gt; As the workflow stands it&#39;s evolved over years, so there&#39;s no reason to expect that I&#39;ve got it &lt;em&gt;right&lt;/em&gt; permanentily this time.&lt;/p&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;h1&gt;Tool improvements&lt;/h1&gt;
&lt;p&gt;There&#39;s definite room for improvement - on the to-do list is to have a go at building my own bookmarking persistence in a more database-y way, for example. The Bear share sheet functionality could benefit from some way of searching or navigating within notes.  Reading content might be a bit easier on a larger iPhone, traded off against the convenience of something that just about fits in a pocket.&lt;/p&gt;
&lt;p&gt;My dream is some way of seamlessly connecting digital workflows and content with an analogue paper notebook, but that feels a bit like nuclear fusion - always due at some intederminate point in the future.&lt;/p&gt;
&lt;h1&gt;Overall&lt;/h1&gt;
&lt;p&gt;Is this perfect? No. Would this work for anyone else? Probably not, everybody&#39;s requirements are slightly different and the point about a workflow that &lt;em&gt;sticks&lt;/em&gt; is that it has to fit with the needs of the individual.&lt;/p&gt;
</content>
  </entry>
  <entry>
    <title>Half-Assing a Web Game with ChatGPT and Friends</title>
    <link href="https://tp-11ty.onrender.com/blog/2024-06-20-election-night-bingo/"/>
    <updated>2024-06-20T00:00:00Z</updated>
    <id>https://tp-11ty.onrender.com/blog/2024-06-20-election-night-bingo/</id>
    <content type="html">&lt;p&gt;I’ve been playing around with AI tools to help create software for a while, but the upcoming UK election was the kick-in-the-behind that I needed to try something a bit more serious.&lt;/p&gt;
&lt;p&gt;I had an &lt;a href=&quot;https://wereyouupforportillo.uk/&quot;&gt;idea for a silly election night distraction&lt;/a&gt; - a sort of bingo-style web game that you could use to track the unfolding catastrophe for the ruling Conservative Party whilst knocking back shots as prominent politicians were cast into the political oblivion.&lt;/p&gt;
&lt;p&gt;My front end web development skills are not sufficiently up-to-date to build that in the limited time available, so I figured I’d try to achieve that by leaning on ChatGPT, Perplexity and Mistral to help me out.&lt;/p&gt;
&lt;p&gt;&lt;a href=&quot;https://wereyouupforportillo.uk/&quot;&gt;It worked, as does the site (after a fashion)&lt;/a&gt;. And the experience was a fascinating one.&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://tp-11ty.onrender.com/wyufp/wyufp.png&quot; alt=&quot;repair&quot; title=&quot;Were You Up For Portillo&quot; /&gt;&lt;/p&gt;
&lt;p&gt;There are definitely benefits in being able to get LLMs to help out:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;I was way quicker than I would have been, had I only had documentation and tutorials to call on&lt;/li&gt;
&lt;li&gt;I could get going with a surface understanding of what I was trying to achieve, once I’d broken the problem down into the main building blocks&lt;/li&gt;
&lt;li&gt;In the end, I delivered a working solution before I lost patience (this isn’t specifically LLM-related, but there is definitely a mean-time-to-giving-up with side projects like this if you can’t get results within a certain timeframe.&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;But there are also downsides:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;I ended up down several dead ends due to the LLM hallucinating - the sheer confidence with which an LLM delivers an answer makes it very easy to blindly cut-and-paste, and it’s not always obvious where the model lost the plot&lt;/li&gt;
&lt;li&gt;My awareness of hallucination potential meant it wasn’t always obvious where the LLM was right and the problem was that I’d introduced the bug&lt;/li&gt;
&lt;li&gt;Using a single LLM didn’t really work, because of the dead-end problem - what worked around this was having several LLMs on the go simultaneously. As they didn’t hallucinate consistently, asking the same question to another model was generally the route out of the dead-end.&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;In summary, then:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;I got something built quicker&lt;/li&gt;
&lt;li&gt;The quality is what you’d expect from an enthusiastic amateur - it’s a long way from robust, tested, well-architected production code&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;That last point is the main problem.&lt;/p&gt;
&lt;p&gt;LLMs are the junk food of the software industry. We rely on them because they’re an initially satisfying quick fix, and just like junk food, you’ll suffer long-term consequences if you rely on them too much.&lt;/p&gt;
&lt;p&gt;LLM-generated code is the result of feeding together different solutions to the same problem through a statistical mincer, and the results are not going to be consistent in terms of the small details that can make the difference between a robust code base and shoddy one.&lt;/p&gt;
&lt;p&gt;Aspects like naming conventions, and error handling patterns, and a myriad of other seemingly-trivial factors: they might not make the different between code that works or not, but they WILL make the challenge of maintaining and extending the code much much harder.&lt;/p&gt;
&lt;p&gt;The bigger problem, though, is with the human in the loop.&lt;/p&gt;
&lt;p&gt;If the solution has been glued together with generated code, you’re probably won’t have the conceptual understanding of the structure that’s needed to reliably understand the solution as a whole.&lt;/p&gt;
&lt;p&gt;Code is important to a product, but building it relies on developing and maintaining intrinsic mental models  - you have to be able to conceive and visualise abstract strictures of data and flows in your minds eye.&lt;/p&gt;
&lt;p&gt;Database records don’t exist in any meaningful physical sense, for example - to understand the structure of the data you’re working with, you have to be able to visualise abstract structures and “spin” them in different directions as you’re working with them. To a very large extent, that’s THE key skill for developers - technical knowledge will get you so far, but lacking that kind of mental ability is going to be a significant handicap.&lt;/p&gt;
&lt;p&gt;That skill is as good a definition of general intelligence as I can think of, so you can see where I’m going here. If general intelligence is needed to build robust software, and LLMs aren’t generally intelligent by any practical measure, you can’t rely on them to write your software for you.&lt;/p&gt;
&lt;p&gt;LLMs will speed you up, but in that sense they’re not so different to a better Google search, or improved abstractions from the underlying technology like libraries and frameworks.&lt;/p&gt;
&lt;p&gt;I don’t expect software engineering is going to go the way of buggy whip manufacturing for a while, and hopefully not in the time I’ve got left in the industry. It will change, just like it has constantly for the last 60 or 70 years, but I wouldn’t bet on the future of an organisations who are currently rushing to replace their software developers with ChatGPT licenses.&lt;/p&gt;
</content>
  </entry>
  <entry>
    <title>Running Jest tests alongside an Express server</title>
    <link href="https://tp-11ty.onrender.com/blog/2024-05-29-testing-servers/"/>
    <updated>2024-05-29T00:00:00Z</updated>
    <id>https://tp-11ty.onrender.com/blog/2024-05-29-testing-servers/</id>
    <content type="html">&lt;p&gt;Here&#39;s the scenario: you&#39;ve got an Express project that you&#39;re testing with Jest. You&#39;ve got the server running, fire off your test suite, and you hit this error:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;listen EADDRINUSE: address already in use :::3000
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;The root cause is that your test suite is trying to start a server instance on the default port, and failing because there&#39;s already a server running on that port.&lt;/p&gt;
&lt;p&gt;The workaround is to persuade Jest to start its server on a different port so that it can co-exist with the production server.&lt;/p&gt;
&lt;p&gt;That needs a bit of adaption in the server, the test suite and &lt;code&gt;package.json&lt;/code&gt;.&lt;/p&gt;
&lt;h1&gt;1. Updating the server code&lt;/h1&gt;
&lt;p&gt;Start by defining a const for the port number:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;dotenv.config();
const port = process.env.PORT || 3000;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Now wrap the server in a &lt;code&gt;startServer&lt;/code&gt; function:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;export const app: Express = express();
<p>export function startServer(port: any) {
return app.listen(port, () =&amp;gt; {
console.log(<code>Server is running on port ${port}</code>);
});
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;And start the server, but only if we're not in the test environment :&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;if (process.env.NODE_ENV !== 'test') {
startServer(port);
}
&lt;/code&gt;&lt;/pre&gt;
&lt;h1&gt;2. Updating the test suite&lt;/h1&gt;
&lt;p&gt;Add a &lt;code&gt;beforeAll()&lt;/code&gt; step to start the server on a testing port:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;let server: any;
const testPort = 4000;</p>
<p>beforeAll((done) =&amp;gt; {
// Start server on non-standard port
server = startServer(testPort);
done();
});
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;and an &lt;code&gt;afterAll()&lt;/code&gt; step to tear down the server after the tests have run:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;afterAll((done) =&amp;gt; {
// Clean up all nocks
server.close(done);
});
&lt;/code&gt;&lt;/pre&gt;
&lt;h1&gt;3. Updating &lt;code&gt;package.json&lt;/code&gt;&lt;/h1&gt;
&lt;p&gt;Specify the Node environment in the &lt;code&gt;test&lt;/code&gt; command:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;&amp;quot;scripts&amp;quot;: {
&amp;quot;server&amp;quot;: &amp;quot;tsc &amp;amp;&amp;amp; node ./dist/server.js&amp;quot;,
&amp;quot;build&amp;quot;: &amp;quot;tsc&amp;quot;,
&amp;quot;dev&amp;quot;: &amp;quot;NODE_ENV=dev node --env-file=.env --watch -r ts-node/register src/server.ts&amp;quot;,
&amp;quot;test&amp;quot;: &amp;quot;NODE_ENV=test jest --verbose --detectOpenHandles&amp;quot;,
&amp;quot;test:watch&amp;quot;: &amp;quot;NODE_ENV=test jest --watch --verbose&amp;quot;,
&amp;quot;lint&amp;quot;: &amp;quot;eslint 'src/**/*.{js,ts}'&amp;quot;
},
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;This sets the &lt;code&gt;NODE_ENV&lt;/code&gt; var in the environment spawned by the test process, which in turn forces the server to start on a non-production port.&lt;/p&gt;
&lt;h1&gt;Summary&lt;/h1&gt;
&lt;p&gt;The complete files should look something like this:&lt;/p&gt;
&lt;h2&gt;&lt;code&gt;server.ts&lt;/code&gt;&lt;/h2&gt;
&lt;pre&gt;&lt;code&gt;import dotenv from 'dotenv';
import express, { Express, Request, Response } from &amp;quot;express&amp;quot;;</p>
<p>dotenv.config();</p>
<p>const port = process.env.PORT || 3000;</p>
<p>export const app: Express = express();</p>
<p>export function startServer(port: any) {
return app.listen(port, () =&amp;gt; {
console.log(<code>Server is running on port ${port}</code>);
});
}</p>
<p>if (process.env.NODE_ENV !== 'test') {
startServer(port);
}</p>
<p>// ROUTES GO HERE</p>
<p>&lt;/code&gt;&lt;/pre&gt;
&lt;h2&gt;&lt;code&gt;server.test.ts&lt;/code&gt;&lt;/h2&gt;
&lt;pre&gt;&lt;code&gt;import { app, startServer } from '../src/server';</p>
<p>let server: any;
const testPort = 4000;</p>
<p>beforeAll((done) =&amp;gt; {
// Start server on non-standard port
server = startServer(testPort);
done();
});</p>
<p>afterAll((done) =&amp;gt; {
server.close(done);
});</p>
<p>describe('The tests...', () =&amp;gt; {
// TESTS GO HERE
});</p>
<p>&lt;/code&gt;&lt;/pre&gt;
&lt;h2&gt;&lt;code&gt;package.json&lt;/code&gt;&lt;/h2&gt;
&lt;pre&gt;&lt;code&gt;{
...
&amp;quot;scripts&amp;quot;: {
&amp;quot;server&amp;quot;: &amp;quot;tsc &amp;amp;&amp;amp; node ./dist/server.js&amp;quot;,
&amp;quot;build&amp;quot;: &amp;quot;tsc&amp;quot;,
&amp;quot;dev&amp;quot;: &amp;quot;NODE_ENV=development node --env-file=.env --watch -r ts-node/register src/server.ts&amp;quot;,
&amp;quot;test&amp;quot;: &amp;quot;jest --verbose --detectOpenHandles&amp;quot;,
&amp;quot;test:watch&amp;quot;: &amp;quot;NODE_ENV=test jest --watch --verbose&amp;quot;,
&amp;quot;lint&amp;quot;: &amp;quot;eslint 'src/**/*.{js,ts}'&amp;quot;
},
...
}
&lt;/code&gt;&lt;/pre&gt;
</content>
</entry></p>
  <entry>
    <title>Data Rention</title>
    <link href="https://tp-11ty.onrender.com/blog/2024-05-01-data-rention/"/>
    <updated>2024-05-01T00:00:00Z</updated>
    <id>https://tp-11ty.onrender.com/blog/2024-05-01-data-rention/</id>
    <content type="html">&lt;p&gt;Designing and implementing data security gets a whole lot easier if you start the exercise with the question &amp;quot;Why the hell are we keeping this data in the first place?&amp;quot;&lt;/p&gt;
</content>
  </entry>
  <entry>
    <title>In Praise of My Toaster</title>
    <link href="https://tp-11ty.onrender.com/blog/2024-04-07-in-praise-of-my-toaster/"/>
    <updated>2024-04-07T00:00:00Z</updated>
    <id>https://tp-11ty.onrender.com/blog/2024-04-07-in-praise-of-my-toaster/</id>
    <content type="html">&lt;p&gt;&lt;img src=&quot;https://tp-11ty.onrender.com/toaster/toaster.png&quot; alt=&quot;toaster&quot; title=&quot;my toaster&quot; /&gt;&lt;/p&gt;
&lt;p&gt;This is my toaster.&lt;/p&gt;
&lt;p&gt;It’s the oldest gadget I own.&lt;/p&gt;
&lt;p&gt;It&#39;s got a retro style all of it&#39;s own, so it&#39;s immune to the currents and trends of fashion.&lt;/p&gt;
&lt;p&gt;Over the years it&#39;s developed a patina of scratches and dents that only add to the aesthetic. Every dent a story, like the ding in the top where I dropped my favourite mug and saw it explode into fragments.&lt;/p&gt;
&lt;p&gt;And while it was several times the price of something made of plastic, it&#39;s been amortised over 20 years and has probably worked out cheaper in the long run.&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://tp-11ty.onrender.com/toaster/repair.png&quot; alt=&quot;repair&quot; title=&quot;repairing&quot; /&gt;&lt;/p&gt;
&lt;p&gt;Last week I had to replace the element, which was a half-hour job involving a screwdriver and a pair of pliers. Now it&#39;s good for another 20 years.&lt;/p&gt;
&lt;p&gt;In the process of replacing the broken part, I realised that in many ways, this might be the perfect gadget.&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://tp-11ty.onrender.com/toaster/element.png&quot; alt=&quot;element&quot; title=&quot;element&quot; /&gt;&lt;/p&gt;
&lt;p&gt;As a toaster, it works exceptionally well.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;It gives feedback:&lt;/strong&gt;  The timer is clockwork, and the operation is simple. Turn further for darker toast. Once you&#39;ve decided on your perfect level of toasting, that&#39;s embedded in memory as an angle of the wrist. You don&#39;t even need to look at the numbers - optimal setting is completely haptic.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;It doesn&#39;t need monitoring:&lt;/strong&gt; The buzzing of the clockwork provides an audible signal that it&#39;s running. As the timer runs down towards the end, the sound subtly changes. The end of the cycle is announced with a solid clunk as a finale, if the olafactory feedback provided by the smell wasn&#39;t enough.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;It&#39;s easy to intervene:&lt;/strong&gt; The lever that flips the toast up out of the slots is independent of the timer, so you can check the degree of browning without interrupting operations. That&#39;s also a failsafe - if you&#39;ve miscalculated the timings, you can just bash down on the knob and the slices will be ejected out before burning too badly. Bash enthusiastically enough and time it right, and you can catch them on a plate&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://tp-11ty.onrender.com/toaster/controls.png&quot; alt=&quot;controls&quot; title=&quot;controls&quot; /&gt;&lt;/p&gt;
&lt;p&gt;As an example of a gadget, it&#39;s also exceptional.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;It&#39;s simple:&lt;/strong&gt; Aside from the element it’s completely mechanical. There&#39;s no electronics to fail silently, or software to be buggy.Electrical problems can be diagnosed with simple tools. It doesn&#39;t take anything more sophisticated than a screwdriver to disassemble. The way it comes apart and goes back together again is completely obvious.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;It&#39;s fixable:&lt;/strong&gt; It&#39;s non-obsolescent. Components like the element and the clockwork mechanism will eventually fail because they&#39;re under stress - but they are readily-replaceable because the manufacturer has committed to maintaining enough inventory. At a push, it would be possible to create ersatz parts myself - rewinding an element with &lt;a href=&quot;https://www.ebay.de/sch/i.html?_from=R40&amp;amp;_trksid=p2332490.m570.l1313&amp;amp;_nkw=nichrome+draht&amp;amp;_sacat=0&quot;&gt;nichrome wire&lt;/a&gt; or &lt;a href=&quot;https://www.yeggi.com/q/dualit/&quot;&gt;3D-printing a plastic part&lt;/a&gt;.&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://tp-11ty.onrender.com/toaster/screws.png&quot; alt=&quot;screws&quot; title=&quot;screws&quot; /&gt;&lt;/p&gt;
&lt;p&gt;There are some things it can’t do. It can’t send me push notifications when the toast is ready, for example. But in some ways this is actually a net positive. If this toaster &lt;em&gt;was&lt;/em&gt; online, it would be a countdown to obsolescence. The functionality would rely on some kind of backend web service that would last as long as the provider stayed interested. It would be at constant risk of enshittification through upsells, or my details being leaked and payments hijacked.&lt;/p&gt;
&lt;p&gt;Do I actually &lt;em&gt;need&lt;/em&gt; a push notification from my toaster. Can’t I rely on the audible cues of the clockwork timer, and the olfactory feedback of the toast browning? If I &lt;em&gt;really&lt;/em&gt; wanted the connectivity, I could plug it into a current-sensing socket, and fire a message when the element stopped drawing power.&lt;/p&gt;
&lt;p&gt;That would have the advantage of decoupling the problems - the  toaster continues to do what the toaster is meant to do, and nothing more. I could switch the digital parts at will to mitigate the obsolescence and enshittification, and introduce new functionality as it develops. Rather than throwing the toaster away because it&#39;s not metaverse-compatible, augment it with the components needed to bridge the gap between the physical and virtual worlds.&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://tp-11ty.onrender.com/toaster/toast.png&quot; alt=&quot;toast&quot; title=&quot;toast&quot; /&gt;&lt;/p&gt;
&lt;p&gt;There&#39;s a temptation to take the analogue approach to extremes, and slip into a kind of neo-luddite mindset that rejects any kind of connectivity, or to assume enshittification is the ultimate fate of any service. But there are some situations where it&#39;s very hard to see &lt;em&gt;why&lt;/em&gt; there could be digital benefits, and the creation of two slices of nicely-browned toast has to be one of them.&lt;/p&gt;
</content>
  </entry>
  <entry>
    <title>Does It Move A Scooter?</title>
    <link href="https://tp-11ty.onrender.com/blog/2024-04-05-does-it-move-a-scooter/"/>
    <updated>2024-04-05T00:00:00Z</updated>
    <id>https://tp-11ty.onrender.com/blog/2024-04-05-does-it-move-a-scooter/</id>
    <content type="html">&lt;p&gt;The hardest problem in a startup is figuring out what not to do.&lt;/p&gt;
&lt;p&gt;Unless you’re very lucky, you’re resource-constrained - there simply isn’t enough money, time or people to do everything that &lt;em&gt;could&lt;/em&gt; be done. At the same time, every stakeholder has their own opinion about what’s most important.&lt;/p&gt;
&lt;p&gt;There’s the ever-present risk of doing the wrong thing, or worse, getting stuck in an analysis trap and not doing anything.&lt;/p&gt;
&lt;p&gt;What you need is a quick rule-of-thumb that you can use to gauge whether something’s worth doing or not.&lt;/p&gt;
&lt;p&gt;I learned this the hard way working on the project from hell - building an e-scooter sharing service which kicked off in a February and absolutely had to be up and running by that summer.&lt;/p&gt;
&lt;p&gt;Every aspect of the service was built from scratch, from the hardware platform, to the mobile app, to the payment system.&lt;/p&gt;
&lt;p&gt;Unsurprisingly, it was chaos - that scope would have been a challenge for a team twice the size with a timeline twice as long.&lt;/p&gt;
&lt;p&gt;We spent a lot of time trying to figure out what needed to be built and what could be left for later, and the process of deciding which was which took almost as long as the building did. It also caused massive amounts of friction and argument.&lt;/p&gt;
&lt;p&gt;After a particularly fractious debate, probably about something disproportionately minor, we hit the point where everyone looked at each other and said the same thing - “there has to be a better way of deciding these things”&lt;/p&gt;
&lt;p&gt;I can’t remember who it was who came up with the idea, but it was simple and to the point:&lt;/p&gt;
&lt;p&gt;If there was disagreement about whether something should be done, we ask a question to short-cut the debate.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;“Does it move a scooter?”&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;That was a simple phrase, but it turned out to be a small act of genius. Within a day or two, it was written in huge multicolor letters across a whiteboard, and it was our standard way of prioritizing.&lt;/p&gt;
&lt;p&gt;The approach works on multiple levels:&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;It’s focused&lt;/strong&gt;. The goal of the project was to get people riding scooters, and scooters that can’t move can’t be ridden.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;It’s simple.&lt;/strong&gt; It doesn’t need a lengthy analysis process; there’s no scoring matrices or cost-benefit analysis. Ultimately it’s a ‘yes’ or ‘no’. True, it does strip nuance from complex situations, but it also encourages getting to the point.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;It’s quick&lt;/strong&gt;. There’s a finite amount of time you can spend arguing as a team about a simple question. If the answer isn’t quick, it’s a hint that the issue isn’t clear enough or the understanding isn’t shared.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;It’s communicable&lt;/strong&gt;. We ended up with it as a kind of team slogan, and it was so simple that it could be explained to business stakeholders 🤪&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;It’s uniting&lt;/strong&gt;. In adverse situations like crunch projects, you need things that the team can unite around - and small-scale rituals like resolving arguments by pointing to the whiteboard and chanting “does it move a scooter” in unison can have a disproportionately-large effect on maintaining morale.&lt;/p&gt;
&lt;p&gt;&lt;em&gt;That&lt;/em&gt; exact phrase is only going to work for you if you’re in the scooter business, but there will be an equivalent in your domain.&lt;/p&gt;
&lt;p&gt;There’s a temptation to make strategy bigger and more complex than it needs to be, because strategy is supposed to be complicated. But early stage startups are all about survival, so making things simple speeds things up.&lt;/p&gt;
</content>
  </entry>
  <entry>
    <title>Interacting with a JMAP API - a rapid-start</title>
    <link href="https://tp-11ty.onrender.com/blog/2024-03-29-jmap/"/>
    <updated>2024-03-29T00:00:00Z</updated>
    <id>https://tp-11ty.onrender.com/blog/2024-03-29-jmap/</id>
    <content type="html">&lt;p&gt;&lt;a href=&quot;https://en.wikipedia.org/wiki/Internet_Message_Access_Protocol&quot;&gt;IMAP&lt;/a&gt; is one of the predominant email protocols out there, but it&#39;s a) very old and b) somewhat painful to work with.  To address some of these problems, &lt;a href=&quot;https://jmap.io/index.html&quot;&gt;JMAP&lt;/a&gt; was designed as IMAP-for-the-21st-ceentury, particularly for client usage.&lt;/p&gt;
&lt;p&gt;Although it&#39;s an &lt;a href=&quot;https://datatracker.ietf.org/wg/jmap/about/&quot;&gt;official IETF standard&lt;/a&gt;, it doesn&#39;t seem to have got that much traction - but is being used and supported by &lt;a href=&quot;https://fastmail.com/&quot;&gt;Fastmail&lt;/a&gt;, who also happen to be my long-term (nearly 20 years) email provider of choice.&lt;/p&gt;
&lt;p&gt;That means I needed to get to grips with it in order to hack together my &lt;a href=&quot;https://trashpanda.cc/2024/03/bearblogging/&quot;&gt;overly-complicated blog-content-from-markdown hack&lt;/a&gt;.&lt;/p&gt;
&lt;p&gt;The problem with not being widely-adopted is the dearth of actual practical examples for &lt;s&gt;copy-and-pasting&lt;/s&gt; inspiration. What follows is my get-things-up-and-running process, hopefully as a springboard for more complex usecases.&lt;/p&gt;
&lt;h2&gt;Basics&lt;/h2&gt;
&lt;p&gt;You interact with the API by sending HTTP &lt;code&gt;POST&lt;/code&gt; requests with an authorisation header and a JSON payload. The payload contains the command you want to run along with any necessary criteria such as search terms.&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;Fastmail&#39;s authorisation requires an &lt;code&gt;Authorization&lt;/code&gt; header with a value in the format &lt;code&gt;Bearer &amp;lt;API token&amp;gt;&lt;/code&gt;)&lt;/li&gt;
&lt;li&gt;The API token needs to be &lt;a href=&quot;https://www.fastmail.help/hc/en-us/articles/5254602856719-API-tokens&quot;&gt;created on the Fastmail dashboard&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;The method calls are sent as JSON text with an &lt;code&gt;application/json&lt;/code&gt; type in the &lt;code&gt;POST&lt;/code&gt; body&lt;/li&gt;
&lt;/ul&gt;
&lt;h2&gt;Command format&lt;/h2&gt;
&lt;p&gt;The POST body has two parts:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;a &lt;code&gt;using&lt;/code&gt; array, which doesn&#39;t change from method to method and defines the spec being used&lt;/li&gt;
&lt;li&gt;a &lt;code&gt;methodCalls&lt;/code&gt; array which contains the actual methods you want to call, along with their parameters&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;The method calls have a format in the shape of &lt;code&gt;object/action&lt;/code&gt; - objects are &lt;code&gt;Mailbox&lt;/code&gt;, &lt;code&gt;Email&lt;/code&gt;, &lt;code&gt;Thread&lt;/code&gt; and so on; actions are verbs like &lt;code&gt;get&lt;/code&gt;, &lt;code&gt;set&lt;/code&gt;, &lt;code&gt;query&lt;/code&gt; etc.&lt;/p&gt;
&lt;p&gt;The main commands we&#39;ll be using are &lt;code&gt;Mailbox/get&lt;/code&gt;, &lt;code&gt;Email/query&lt;/code&gt; and &lt;code&gt;Email/get&lt;/code&gt;.&lt;/p&gt;
&lt;p&gt;The overall shape of the body looks like this:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;{
  &amp;quot;using&amp;quot;: [
    &amp;quot;urn:ietf:params:jmap:core&amp;quot;,
    &amp;quot;urn:ietf:params:jmap:mail&amp;quot;
  ],
  &amp;quot;methodCalls&amp;quot;: [
    &amp;lt;calls go here&amp;gt;
  ]
}
&lt;/code&gt;&lt;/pre&gt;
&lt;h2&gt;Getting your account ID&lt;/h2&gt;
&lt;p&gt;The first step is to figure out your Fastmail account ID. This requires making an authenticated GET request to &lt;code&gt;https://api.fastmail.com/.well-known/jmap&lt;/code&gt;, which returns a JSON response containing details of your account and what rights the API key allows.&lt;/p&gt;
&lt;p&gt;Buried in middle somewhere is a &lt;code&gt;primaryAccounts&lt;/code&gt; key, which should look something like this:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;&amp;quot;primaryAccounts&amp;quot;: {
    &amp;quot;urn:ietf:params:jmap:core&amp;quot;: &amp;quot;u123456ab&amp;quot;,
    &amp;quot;urn:ietf:params:jmap:mail&amp;quot;: &amp;quot;u123456ab&amp;quot;
  },
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Make a note of the value of the &lt;code&gt;&amp;lt;xxx&amp;gt;:mail&lt;/code&gt; key, you&#39;ll need it for all further requests.&lt;/p&gt;
&lt;h2&gt;Request a list of folders&lt;/h2&gt;
&lt;p&gt;Most processes are going to involve getting a list of available messages in a specific folder, and the prerequisite for that is a folder ID.&lt;/p&gt;
&lt;h3&gt;The request&lt;/h3&gt;
&lt;p&gt;Let&#39;s start by getting a list of folders (aka mailboxes in JMAP-speak). Send this JSON body in a POST request:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;{
  &amp;quot;using&amp;quot;: [
    &amp;quot;urn:ietf:params:jmap:core&amp;quot;,
    &amp;quot;urn:ietf:params:jmap:mail&amp;quot;
  ],
  &amp;quot;methodCalls&amp;quot;: [
    [
      &amp;quot;Mailbox/get&amp;quot;,
      {
        &amp;quot;accountId&amp;quot;: &amp;quot;u123456ab&amp;quot;,
        &amp;quot;ids&amp;quot;: null
      },
      &amp;quot;abc&amp;quot;
    ]
  ]
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Breaking down the &lt;code&gt;methodCalls&lt;/code&gt; section, there&#39;s three parts:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;the command we&#39;re sending, in this case &lt;code&gt;Mailbox/get&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;the parameters for this command, in this case the &lt;code&gt;accountId&lt;/code&gt; and the folders &lt;code&gt;ids&lt;/code&gt; we want to match. By sending &lt;code&gt;null&lt;/code&gt;, the server will respond with a list of all folders/mailboxes&lt;/li&gt;
&lt;li&gt;the trailing &lt;code&gt;abc&lt;/code&gt; is effectively a variable name - the results of this &lt;code&gt;Mailbox/get&lt;/code&gt; command will be available to &lt;em&gt;subsquent&lt;/em&gt; commands by referencing the &lt;code&gt;abc&lt;/code&gt; variable, which allows you to create composite commands that are chained together&lt;/li&gt;
&lt;/ul&gt;
&lt;h3&gt;The response&lt;/h3&gt;
&lt;p&gt;The respose is a blob of JSON containing the &lt;code&gt;methodResponse&lt;/code&gt; array. That in turn contains a list of folders and their various properties - the keys we&#39;re interested in look like this:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;{
  &amp;quot;methodResponses&amp;quot;: [
    [
      &amp;quot;Mailbox/get&amp;quot;,
      {
        &amp;quot;list&amp;quot;: [
          {
            ...
            &amp;quot;name&amp;quot;: &amp;quot;Inbox&amp;quot;,
            &amp;quot;id&amp;quot;: &amp;quot;abcdef1234&amp;quot;,
            ...
          },
        ...
      },
      &amp;quot;abc&amp;quot;
    ]
  ],
  &amp;quot;latestClientVersion&amp;quot;: &amp;quot;&amp;quot;,
  &amp;quot;sessionState&amp;quot;: &amp;quot;cyrus-0;p-e21c23889a;s-6606885adc92256b&amp;quot;
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;The &lt;code&gt;name&lt;/code&gt; key contains the human-readable folder name, and the &lt;code&gt;id&lt;/code&gt; is the corresponding folder ID&lt;/p&gt;
&lt;h2&gt;Getting a list of emails&lt;/h2&gt;
&lt;p&gt;Retrieving a specific email is a two-stage process: finding its ID, then retrieving the email itself:&lt;/p&gt;
&lt;h3&gt;Retrieving a list of emails in a specific folder&lt;/h3&gt;
&lt;p&gt;This uses the &lt;code&gt;Email/query&lt;/code&gt; command with a filter (this example also throws in a sort order for good measure). It&#39;s the same process - make a POST request with the auth headers, and send over the JSON payload in the body.&lt;/p&gt;
&lt;p&gt;This is the full payload:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;{
  &amp;quot;using&amp;quot;: [
    &amp;quot;urn:ietf:params:jmap:core&amp;quot;,
    &amp;quot;urn:ietf:params:jmap:mail&amp;quot;
  ],
  &amp;quot;methodCalls&amp;quot;: [
    [
      &amp;quot;Email/query&amp;quot;,
      {
        &amp;quot;accountId&amp;quot;: &amp;quot;u123456ab&amp;quot;,
        &amp;quot;filter&amp;quot;: {
          &amp;quot;inMailbox&amp;quot;: &amp;quot;abcdef1234&amp;quot;
        },
        &amp;quot;sort&amp;quot;: [
          {
            &amp;quot;property&amp;quot;: &amp;quot;receivedAt&amp;quot;,
            &amp;quot;isAscending&amp;quot;: false
          }
        ],
        &amp;quot;limit&amp;quot;: 500
      },
      &amp;quot;xzy&amp;quot;
    ]
  ]
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Breaking this down, we have:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;a filter, which searches in a specific mailbox with ID &lt;code&gt;abcdef1234&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;a sort order, which in this case is by property &lt;code&gt;receivedAt&lt;/code&gt; and most-recent first&lt;/li&gt;
&lt;li&gt;a limit, in case there&#39;s a lot of results. If that happens, you can start using paging, but that&#39;s beyond the scope of this simple example.&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;The result is a JSON response which looks like this:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;{
  &amp;quot;methodResponses&amp;quot;: [
    [
      &amp;quot;Email/query&amp;quot;,
      {
        &amp;quot;ids&amp;quot;: [
          &amp;quot;Mcae60028353b175d88882abe&amp;quot;,
          &amp;quot;M999a6d73062302c1592daebe&amp;quot;,
          &amp;quot;M38493717a69686cbb4fc4d6f&amp;quot;,
        ],
        &amp;quot;sort&amp;quot;: [
          {
            &amp;quot;isAscending&amp;quot;: false,
            &amp;quot;property&amp;quot;: &amp;quot;receivedAt&amp;quot;
          }
        ],
        &amp;quot;filter&amp;quot;: {
          &amp;quot;inMailbox&amp;quot;: &amp;quot;abcdef1234&amp;quot;
        },
        &amp;quot;position&amp;quot;: 0,
        &amp;quot;collapseThreads&amp;quot;: false,
        &amp;quot;queryState&amp;quot;: &amp;quot;1871939:0&amp;quot;,
        &amp;quot;canCalculateChanges&amp;quot;: true,
        &amp;quot;accountId&amp;quot;: &amp;quot;u123456ab&amp;quot;,
        &amp;quot;total&amp;quot;: 3
      },
      &amp;quot;xyz&amp;quot;
    ]
  ],
  &amp;quot;latestClientVersion&amp;quot;: &amp;quot;&amp;quot;,
  &amp;quot;sessionState&amp;quot;: &amp;quot;cyrus-0;p-e21c23889a;s-6606885adc92256b&amp;quot;
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;The first and most useful part is the &lt;code&gt;ids&lt;/code&gt; array, which contains a list of IDs for actual emails. You also get a confirmation of the sort order and filter that was used, and some metadata about the query itself.&lt;/p&gt;
&lt;p&gt;&lt;code&gt;position&lt;/code&gt; relates to the paging if that took place; &lt;code&gt;collapseThreads&lt;/code&gt; relates to how message threads are handled, and the &lt;code&gt;total&lt;/code&gt; value is the number of records returned which can be useful when processing the results.&lt;/p&gt;
&lt;h3&gt;Retrieving a specific email&lt;/h3&gt;
&lt;p&gt;Now that we&#39;ve got message IDs, we can finish off by retrieving a specific one.  This uses the &lt;code&gt;Email/get&lt;/code&gt; method, to which you pass a specific message ID(s):&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;{
  &amp;quot;using&amp;quot;: [
    &amp;quot;urn:ietf:params:jmap:core&amp;quot;,
    &amp;quot;urn:ietf:params:jmap:mail&amp;quot;
  ],
  &amp;quot;methodCalls&amp;quot;: [
    [
      &amp;quot;Email/get&amp;quot;,
        {
          &amp;quot;accountId&amp;quot;: &amp;quot;abcdef1234&amp;quot;,
          &amp;quot;properties&amp;quot;: null,
          &amp;quot;ids&amp;quot;: [
            &amp;quot;Mcae60028353b175d88882abe&amp;quot;
          ]
        },
      &amp;quot;pqr&amp;quot;
    ]
  ]
}
<p>&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;The result (assuming the message exists) should be a lump of JSON that contains the message itself along with a pile of metadata:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;{
&amp;quot;methodResponses&amp;quot;: [
[
&amp;quot;Email/get&amp;quot;,
{
&amp;quot;accountId&amp;quot;: &amp;quot;abcdef1234&amp;quot;,
&amp;quot;state&amp;quot;: &amp;quot;1872326&amp;quot;,
&amp;quot;notFound&amp;quot;: [],
&amp;quot;list&amp;quot;: [
{
&amp;quot;attachments&amp;quot;: [],
&amp;quot;mailboxIds&amp;quot;: {
&amp;quot;abcdef1234&amp;quot;: true
},
&amp;quot;size&amp;quot;: 91829,
&amp;quot;htmlBody&amp;quot;: [
{
&amp;quot;language&amp;quot;: null,
&amp;quot;location&amp;quot;: null,
&amp;quot;blobId&amp;quot;: &amp;quot;G67f2b026af5a35d77985d0dd57edb93364d2736&amp;quot;,
&amp;quot;name&amp;quot;: null,
&amp;quot;type&amp;quot;: &amp;quot;text/html&amp;quot;,
&amp;quot;cid&amp;quot;: null,
&amp;quot;size&amp;quot;: 61902,
&amp;quot;charset&amp;quot;: &amp;quot;utf-8&amp;quot;,
&amp;quot;partId&amp;quot;: &amp;quot;2&amp;quot;,
&amp;quot;disposition&amp;quot;: null
}
],
&amp;quot;blobId&amp;quot;: &amp;quot;G438f25a1713b0b3445b3ecd76a2d33d43337856&amp;quot;,
&amp;quot;from&amp;quot;: [
{
&amp;quot;email&amp;quot;: &amp;quot;sz-magazin@newsletter.sueddeutsche.de&amp;quot;,
&amp;quot;name&amp;quot;: &amp;quot;SZ-Magazin Das Beste&amp;quot;
}
],
&amp;quot;textBody&amp;quot;: [
{
&amp;quot;disposition&amp;quot;: null,
&amp;quot;size&amp;quot;: 13300,
&amp;quot;charset&amp;quot;: &amp;quot;utf-8&amp;quot;,
&amp;quot;partId&amp;quot;: &amp;quot;1&amp;quot;,
&amp;quot;cid&amp;quot;: null,
&amp;quot;type&amp;quot;: &amp;quot;text/plain&amp;quot;,
&amp;quot;name&amp;quot;: null,
&amp;quot;language&amp;quot;: null,
&amp;quot;location&amp;quot;: null,
&amp;quot;blobId&amp;quot;: &amp;quot;G05569b6bd4104d589a7efdece3d2f5d7dcbfe1e&amp;quot;
}
],
&amp;quot;inReplyTo&amp;quot;: null,
&amp;quot;id&amp;quot;: &amp;quot;M438f25a1713b0b4451b3ecd&amp;quot;,
&amp;quot;sentAt&amp;quot;: &amp;quot;2024-03-30T07:59:53+01:00&amp;quot;,
&amp;quot;to&amp;quot;: [
{
&amp;quot;name&amp;quot;: null,
&amp;quot;email&amp;quot;: &amp;quot;tim@duckett.de&amp;quot;
}
],
&amp;quot;receivedAt&amp;quot;: &amp;quot;2024-03-30T06:59:59Z&amp;quot;,
&amp;quot;sender&amp;quot;: null,
&amp;quot;hasAttachment&amp;quot;: false,
&amp;quot;threadId&amp;quot;: &amp;quot;Td27cfc407a520026&amp;quot;,
&amp;quot;keywords&amp;quot;: {
&amp;quot;$x-me-annot-2&amp;quot;: true,
&amp;quot;$ismailinglist&amp;quot;: true,
&amp;quot;$canunsubscribe&amp;quot;: true
},
&amp;quot;preview&amp;quot;: &amp;quot;Außerdem: Warum das Wort »später« so gefährlich ist Sollte der Newsletter nicht korrekt angezeigt werden, klicken Sie bitte hier Illustration: iStock / by Malte Mueller&amp;quot;,
&amp;quot;subject&amp;quot;: &amp;quot;Lust auf ein neues Leben?&amp;quot;,
&amp;quot;cc&amp;quot;: null,
&amp;quot;bcc&amp;quot;: null,
&amp;quot;messageId&amp;quot;: [
&amp;quot;0.0.1A.354.1DA826FDE08E6A0.0@uspmta120023.emarsys.net&amp;quot;
],
&amp;quot;replyTo&amp;quot;: null,
&amp;quot;bodyValues&amp;quot;: {},
&amp;quot;references&amp;quot;: null
}
]
},
&amp;quot;a&amp;quot;
]
],
&amp;quot;latestClientVersion&amp;quot;: &amp;quot;&amp;quot;,
&amp;quot;sessionState&amp;quot;: &amp;quot;cyrus-0;p-e21c23889a;s-6607b80d3ff0a0fe&amp;quot;
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;This is an example of an HTML-formatted message - there's a text preview which is contained in the &lt;code&gt;preview&lt;/code&gt; key, but there's one more step required to get the content of the message itself. This is available in two formats:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;HTML, which is referred to by the &lt;code&gt;blobId&lt;/code&gt; contained in the &lt;code&gt;htmlBody&lt;/code&gt; section&lt;/li&gt;
&lt;li&gt;text, which is referred to by the &lt;code&gt;blobId&lt;/code&gt; contained in the &lt;code&gt;textBody&lt;/code&gt; section&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;Confusingly, there's a &lt;em&gt;third&lt;/em&gt; &lt;code&gt;blobId&lt;/code&gt; in the &lt;code&gt;list&lt;/code&gt; section which refers to the message itself in case you want retrieve that as a block of text.&lt;/p&gt;
&lt;h3&gt;Retrieving the contents of a specific email&lt;/h3&gt;
&lt;p&gt;This final step is different to the others, because it's a straight-forward &lt;code&gt;GET&lt;/code&gt; request:&lt;/p&gt;
&lt;p&gt;&lt;code&gt;GET https://www.fastmailusercontent.com/jmap/download/&amp;lt;accountId&amp;gt;/&amp;lt;blobId&amp;gt;/&amp;lt;filename&amp;gt;?type=&amp;lt;type&amp;gt;&lt;/code&gt;&lt;/p&gt;
&lt;p&gt;The parameters here are:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;code&gt;accountId&lt;/code&gt; - the account ID as used before&lt;/li&gt;
&lt;li&gt;&lt;code&gt;blobId&lt;/code&gt;- the ID of the specific blob you want, either HTML or text&lt;/li&gt;
&lt;li&gt;&lt;code&gt;filename&lt;/code&gt; - this appears to have no effect as far as I can tell, but needs to be present; you can send any string&lt;/li&gt;
&lt;li&gt;&lt;code&gt;type&lt;/code&gt; - the MIME type in which you want the content encoded. This makes no practical difference in terms of the content returned, but does show up in the &lt;code&gt;content-type&lt;/code&gt; response header in case your code needs this. In this example, it makes most sense to send &lt;code&gt;application/html&lt;/code&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;h3&gt;Getting email attachments&lt;/h3&gt;
&lt;p&gt;Although that final &lt;code&gt;GET&lt;/code&gt; request is the end point if what you want is the message, my usecase was slightly different and I wanted any attachments.&lt;/p&gt;
&lt;p&gt;The good news here is that the process of getting attachments is the same as the process of getting email contents - you send a &lt;code&gt;GET&lt;/code&gt; request with a blob ID.&lt;/p&gt;
&lt;p&gt;Attachment blob IDs will show up in the &lt;code&gt;attachments&lt;/code&gt; array at the top of the email payload:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;{
&amp;quot;methodResponses&amp;quot;: [
[
&amp;quot;Email/get&amp;quot;,
{
&amp;quot;accountId&amp;quot;: &amp;quot;abcdef1234&amp;quot;,
&amp;quot;state&amp;quot;: &amp;quot;1872326&amp;quot;,
&amp;quot;notFound&amp;quot;: [],
&amp;quot;list&amp;quot;: [
{
&amp;quot;attachments&amp;quot;: [
&amp;quot;G67f2b026af5a35d77985d0dd45edb93364d2736&amp;quot;
],
&amp;quot;mailboxIds&amp;quot;: {
&amp;quot;abcdef1234&amp;quot;: true
},
&amp;lt;SNIP&amp;gt;
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Grab the blob ID(s) from the &lt;code&gt;attachments&lt;/code&gt; array, feed these into the &lt;code&gt;GET&lt;/code&gt; requests, and you can download and process attachments as needed.&lt;/p&gt;
&lt;h3&gt;Finishing up&lt;/h3&gt;
&lt;p&gt;This is only the minimal basics needed to get messages and attachments; there's a lot more that the protocol can do. The docs are here, and you can use that information to extend the examples above to build a completely-functional email client if that's what you need.&lt;/p&gt;
&lt;p&gt;There's a &lt;a href=&quot;https://gist.github.com/timd/d34be1cfee9efb17723cfdd3786291e1&quot;&gt;Postman collection of the relevant API calls here&lt;/a&gt;.&lt;/p&gt;
</content>
</entry></p>
  <entry>
    <title>LinkedIn, how I loathe thee</title>
    <link href="https://tp-11ty.onrender.com/blog/2024-03-19-LinkedIn-how-I-loathe-thee/"/>
    <updated>2024-03-19T00:00:00Z</updated>
    <id>https://tp-11ty.onrender.com/blog/2024-03-19-LinkedIn-how-I-loathe-thee/</id>
    <content type="html">&lt;p&gt;LinkedIn sits firmly in the “&lt;em&gt;it has to exist, but wouldn’t life be better if it didn’t have to?&lt;/em&gt;” quadrant of existence.&lt;/p&gt;
&lt;p&gt;For the basics of a network of connections, and a way for others to find me, it works. But every time I close the tab, I can’t help thinking “this should work better than it does”.&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;The endless scrolling encourages mindless interaction. There’s no “end” - it’s never “done” and finished with, so when is “enough” is a &lt;em&gt;decision&lt;/em&gt; I have to make&lt;/li&gt;
&lt;li&gt;The random nature of the feed makes it virtually impossible &lt;em&gt;not&lt;/em&gt; to lose things unless you you remember to bookmark something immediately&lt;/li&gt;
&lt;li&gt;Once a post has gone, you’re stuck with trying to remember enough detail to search&lt;/li&gt;
&lt;li&gt;Speaking of which, LinkedIn search sucks, and sucks badly&lt;/li&gt;
&lt;li&gt;Feed filtering is more unreliable than not, so the default is drown in garbage&lt;/li&gt;
&lt;li&gt;The feed doesn’t appear to prioritise your contacts (I assume to drive more connections)&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;All of this creates the feeling that the interaction patterns are optimised for some product manager’s perception of “engagement”, rather than my utility.&lt;/p&gt;
&lt;p&gt;Fixing this shouldn’t be hard. There’s no reason why a useful UI can’t coexist with the river of broetry from the hustle pros and Gary Vee wannabes.&lt;/p&gt;
&lt;p&gt;Provide a reverse-chronological feed of content from &lt;em&gt;my&lt;/em&gt; contacts, and when it’s all read, it’s done - basically the pattern of Twitter before Elon Musk burned it to the ground.&lt;/p&gt;
</content>
  </entry>
  <entry>
    <title>Bearblogging</title>
    <link href="https://tp-11ty.onrender.com/blog/2024-03-19-Bearblogging/"/>
    <updated>2024-03-19T00:00:00Z</updated>
    <id>https://tp-11ty.onrender.com/blog/2024-03-19-Bearblogging/</id>
    <content type="html">&lt;p&gt;It’s truth universally acknowledged that a person in possession of opposable thumbs an a modicum of programming skills must be in want of an over-elaborate web publishing workflow.&lt;/p&gt;
&lt;p&gt;My basic blog setup is based on Hugo, which is a static site generator that renders Markdown files into web pages. I write the Markdown files locally, then push the changes to GitHub, after which a deployment job in Render is triggered. This checks out the latest revision, builds it, and published the resulting static HTML.&lt;/p&gt;
&lt;p&gt;It all works very well, but it does depend on creating, committing and pushing Markdown files. That’s not easy to do on something that isn’t a laptop - not that Markdown is difficult, but there’s no good combination of both iOS GitHub client and an iOS Markdown editor.&lt;/p&gt;
&lt;p&gt;What iOS and Mac do have is Bear, which is a beautifully-designed notes tool that I use extensively as an outboard brain. It’s ideal for banging out short and medium length files, but it doesn’t fit at all into a Hugo workflow.&lt;/p&gt;
&lt;p&gt;Until now. The process I’ve come up with isn’t exactly elegant, but it is seamless.&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;I write the content in Bear, and email the resulting file to myself as a Markdown file&lt;/li&gt;
&lt;li&gt;My mail server files the email into the right folder based on sender and subject&lt;/li&gt;
&lt;li&gt;Periodically, a cron job wakes up a Node app running in a Docker container on Render by pinging a webhook URL&lt;/li&gt;
&lt;li&gt;The Node app checks for waiting email; grabs the message; downloads the attachment, renders it into a Hugo-style Markdown file, saves it locally, then pushes the new file up to GitHub.&lt;/li&gt;
&lt;li&gt;The my website’s Render project watches its repo, and whenever a change is detected it runs the deploy process I described above.&lt;/li&gt;
&lt;li&gt;I can force the whole process by pinging the webhook URL to wake the Node app up manually.&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;No, this was not exactly trivial to setup.&lt;/p&gt;
&lt;p&gt;Yes, there are quite a lot of moving parts.&lt;/p&gt;
&lt;p&gt;Yes, I probably did take it all a bit far. It’s a bit over the top just for the sake of sticking with a specific text editor. On the other hand, it was a learning process, particularly the “devopsy” bits of building and deploying Docker containers; and wrangling the connections to IMAP servers.&lt;/p&gt;
&lt;p&gt;Will I actually use this, to keep this site updated more often? To be determined…&lt;/p&gt;
</content>
  </entry>
  <entry>
    <title>Sleepwalking into a new dark age</title>
    <link href="https://tp-11ty.onrender.com/blog/2024-03-08-british-library-attack-report/"/>
    <updated>2024-03-08T00:00:00Z</updated>
    <id>https://tp-11ty.onrender.com/blog/2024-03-08-british-library-attack-report/</id>
    <content type="html">&lt;p&gt;The &lt;a href=&quot;https://www.bl.uk/home/british-library-cyber-incident-review-8-march-2024.pdf&quot;&gt;report&lt;/a&gt; into the &lt;a href=&quot;https://www.theguardian.com/books/2023/oct/31/british-library-suffering-major-technology-outage-after-cyber-attack&quot;&gt;extensive attack&lt;/a&gt; which took out the &lt;a href=&quot;https://www.bl.uk/&quot;&gt;British Library&lt;/a&gt; has just been published, and it doesn’t make for good reading. It’s another data point that that reinforces my theory that we’re sleepwalking into a new dark ages.&lt;/p&gt;
&lt;p&gt;Briefly, the British Library - the ultimate custodian of every printed and published resource in Britain since the 18th century and before - was &lt;a href=&quot;https://www.bl.uk/cyber-incident/&quot;&gt;crippled by an attack&lt;/a&gt; that brought down systems and exfiltrated gigabytes of data. It’s still struggling to get all systems back online, and there’s no timeline for when - or even if - the damage will finally be repaired.&lt;/p&gt;
&lt;p&gt;The report is partial, because much of the evidence of how the attack took place has been lost in the rubble. But there’s some depressing conclusions.&lt;/p&gt;
&lt;p&gt;The attack succeeded in the first place because systems were complex, outdated and unmaintained. Cost pressures led to a tangle of contractors and suppliers, which in turn allowed otherwise-addressable vulnerabilities to creep in, and resulted in a lack of institutional knowledge about how systems operated.&lt;/p&gt;
&lt;p&gt;The damage was compounded by some systems being unrecoverable simply because they’re now obsolete - either they’re no longer maintained, or they’re incompatible with available operating systems.&lt;/p&gt;
&lt;p&gt;The fact that the report’s been published is positive - it’s hard to imagine a commercial organization washing its laundry in public like this. There’s a chance that lessons will be learned and the future will be different, although past experience suggests that’s a lot easier to promise than deliver.&lt;/p&gt;
&lt;p&gt;What strikes me reading the report is that at the same time that the sheer volume of content being created is accelerating, maintaining it for the long-term is getting increasing difficult. A document from 500 years ago can still be read and understood; a file stored on a medium from the last decade is probably lost forever now.&lt;/p&gt;
</content>
  </entry>
  <entry>
    <title>Setting up a Typescript-based Node project: the basics</title>
    <link href="https://tp-11ty.onrender.com/blog/2024-02-25-hello-world-typescript-project/"/>
    <updated>2024-02-25T00:00:00Z</updated>
    <id>https://tp-11ty.onrender.com/blog/2024-02-25-hello-world-typescript-project/</id>
    <content type="html">&lt;p&gt;&lt;em&gt;tl;dr: This is the simplest way I know of setting up a Node projeect using Typescript from scratch.&lt;/em&gt;&lt;/p&gt;
&lt;p&gt;My weapon of choice for most development these days is Node, and because my formative code years were spent fighting with type-safe languages, I prefer Typescript to vanilla Javascript.&lt;/p&gt;
&lt;p&gt;I start a lot more projects that I finish, but despite the number of times I&#39;ve set things up from scratch, I always end up forgetting some step some point along the way. The point of this post is a note-to-self with a process to follow; and maybe an aide-memoire for others.&lt;/p&gt;
&lt;p&gt;There&#39;s a &lt;a href=&quot;https://github.com/timd/boilerplate-typescript&quot;&gt;cloneable repo&lt;/a&gt; containing this setup on Github.&lt;/p&gt;
&lt;h1&gt;The process&lt;/h1&gt;
&lt;p&gt;There are three stages:&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;Basic directory and Git things&lt;/li&gt;
&lt;li&gt;Node and Typescript&lt;/li&gt;
&lt;li&gt;Wiring it all up&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;Strictly speaking, step 3 isn&#39;t essential, but it&#39;s nice to make sure things work.&lt;/p&gt;
&lt;h2&gt;Directory and Git things&lt;/h2&gt;
&lt;ol&gt;
&lt;li&gt;Create a directory for the project:&lt;/li&gt;
&lt;/ol&gt;
&lt;pre class=&quot;language-go&quot; tabindex=&quot;0&quot;&gt;&lt;code class=&quot;language-go&quot;&gt;mkdir template&lt;span class=&quot;token operator&quot;&gt;-&lt;/span&gt;node&lt;span class=&quot;token operator&quot;&gt;-&lt;/span&gt;project
cd template&lt;span class=&quot;token operator&quot;&gt;-&lt;/span&gt;node&lt;span class=&quot;token operator&quot;&gt;-&lt;/span&gt;project&lt;/code&gt;&lt;/pre&gt;
&lt;ol start=&quot;2&quot;&gt;
&lt;li&gt;Initialise Git&lt;/li&gt;
&lt;/ol&gt;
&lt;pre class=&quot;language-go&quot; tabindex=&quot;0&quot;&gt;&lt;code class=&quot;language-go&quot;&gt;git init&lt;/code&gt;&lt;/pre&gt;
&lt;ol start=&quot;3&quot;&gt;
&lt;li&gt;Add a &lt;code&gt;.gitignore&lt;/code&gt; for later:&lt;/li&gt;
&lt;/ol&gt;
&lt;pre class=&quot;language-go&quot; tabindex=&quot;0&quot;&gt;&lt;code class=&quot;language-go&quot;&gt;touch &lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;gitignore&lt;/code&gt;&lt;/pre&gt;
&lt;ol start=&quot;4&quot;&gt;
&lt;li&gt;Add a &lt;code&gt;README&lt;/code&gt; for later:&lt;/li&gt;
&lt;/ol&gt;
&lt;pre class=&quot;language-go&quot; tabindex=&quot;0&quot;&gt;&lt;code class=&quot;language-go&quot;&gt;touch README&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;md&lt;/code&gt;&lt;/pre&gt;
&lt;h2&gt;Setup the Node project&lt;/h2&gt;
&lt;ol&gt;
&lt;li&gt;Initialise the Node project:&lt;/li&gt;
&lt;/ol&gt;
&lt;pre class=&quot;language-go&quot; tabindex=&quot;0&quot;&gt;&lt;code class=&quot;language-go&quot;&gt;npm init &lt;span class=&quot;token operator&quot;&gt;-&lt;/span&gt;y&lt;/code&gt;&lt;/pre&gt;
&lt;ol start=&quot;2&quot;&gt;
&lt;li&gt;This will create a &lt;code&gt;package.json&lt;/code&gt; file that contains default settings for things like the project name, version, license and so on. You can edit these as needed later.&lt;/li&gt;
&lt;/ol&gt;
&lt;h2&gt;Set up Typescript&lt;/h2&gt;
&lt;ol&gt;
&lt;li&gt;Install Typescript, the Node dependencies, and the Node types:&lt;/li&gt;
&lt;/ol&gt;
&lt;pre class=&quot;language-go&quot; tabindex=&quot;0&quot;&gt;&lt;code class=&quot;language-go&quot;&gt;npm i &lt;span class=&quot;token operator&quot;&gt;-&lt;/span&gt;D typescript ts&lt;span class=&quot;token operator&quot;&gt;-&lt;/span&gt;node @types&lt;span class=&quot;token operator&quot;&gt;/&lt;/span&gt;node&lt;/code&gt;&lt;/pre&gt;
&lt;ol start=&quot;2&quot;&gt;
&lt;li&gt;Initialise Typescript:&lt;/li&gt;
&lt;/ol&gt;
&lt;pre class=&quot;language-go&quot; tabindex=&quot;0&quot;&gt;&lt;code class=&quot;language-go&quot;&gt;npx tsc &lt;span class=&quot;token operator&quot;&gt;--&lt;/span&gt;init&lt;/code&gt;&lt;/pre&gt;
&lt;ol start=&quot;4&quot;&gt;
&lt;li&gt;Add an &lt;code&gt;.env&lt;/code&gt; file:&lt;/li&gt;
&lt;/ol&gt;
&lt;pre class=&quot;language-go&quot; tabindex=&quot;0&quot;&gt;&lt;code class=&quot;language-go&quot;&gt;touch &lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;env&lt;/code&gt;&lt;/pre&gt;
&lt;ol start=&quot;5&quot;&gt;
&lt;li&gt;Add a placeholder environment variable in the &lt;code&gt;.env&lt;/code&gt; file:&lt;/li&gt;
&lt;/ol&gt;
&lt;pre class=&quot;language-go&quot; tabindex=&quot;0&quot;&gt;&lt;code class=&quot;language-go&quot;&gt;GREETING&lt;span class=&quot;token operator&quot;&gt;=&lt;/span&gt;world&lt;/code&gt;&lt;/pre&gt;
&lt;ol start=&quot;6&quot;&gt;
&lt;li&gt;Update the &lt;code&gt;package.json&lt;/code&gt; file to add a couple of extra settings:&lt;/li&gt;
&lt;/ol&gt;
&lt;ul&gt;
&lt;li&gt;Add an &lt;code&gt;engines&lt;/code&gt; section&lt;/li&gt;
&lt;/ul&gt;
&lt;pre class=&quot;language-go&quot; tabindex=&quot;0&quot;&gt;&lt;code class=&quot;language-go&quot;&gt;&lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt;
  &lt;span class=&quot;token string&quot;&gt;&quot;engines&quot;&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt;
    &lt;span class=&quot;token string&quot;&gt;&quot;node&quot;&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;token string&quot;&gt;&quot;&gt;=20.6.0&quot;&lt;/span&gt;
  &lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt;
  &lt;span class=&quot;token string&quot;&gt;&quot;name&quot;&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;token string&quot;&gt;&quot;template-node-project&quot;&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt;
  &lt;span class=&quot;token string&quot;&gt;&quot;version&quot;&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;token string&quot;&gt;&quot;1.0.0&quot;&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt;
  &lt;span class=&quot;token string&quot;&gt;&quot;description&quot;&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;token string&quot;&gt;&quot;&quot;&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt;
  &lt;span class=&quot;token string&quot;&gt;&quot;main&quot;&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;token string&quot;&gt;&quot;index.js&quot;&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt;
  &lt;span class=&quot;token string&quot;&gt;&quot;scripts&quot;&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt;
    &lt;span class=&quot;token string&quot;&gt;&quot;build&quot;&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;token string&quot;&gt;&quot;tsc&quot;&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt;
    &lt;span class=&quot;token string&quot;&gt;&quot;dev&quot;&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;token string&quot;&gt;&quot;node --env-file=.env --watch -r ts-node/register src/index.ts&quot;&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt;
    &lt;span class=&quot;token string&quot;&gt;&quot;test&quot;&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;token string&quot;&gt;&quot;echo &#92;&quot;Error: no test specified&#92;&quot; &amp;amp;&amp;amp; exit 1&quot;&lt;/span&gt;
  &lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt;
  &lt;span class=&quot;token string&quot;&gt;&quot;keywords&quot;&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;[&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;]&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt;
  &lt;span class=&quot;token string&quot;&gt;&quot;author&quot;&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;token string&quot;&gt;&quot;&quot;&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt;
  &lt;span class=&quot;token string&quot;&gt;&quot;license&quot;&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;token string&quot;&gt;&quot;ISC&quot;&lt;/span&gt;
&lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;
&lt;ul&gt;
&lt;li&gt;this defines:
&lt;ul&gt;
&lt;li&gt;uses &lt;code&gt;tsc&lt;/code&gt; as the compiler&lt;/li&gt;
&lt;li&gt;loads the &lt;code&gt;.env&lt;/code&gt; file to set environment variables&lt;/li&gt;
&lt;li&gt;runs the Node process in &#39;watch&#39; mode, so code changes are automatically reloaded&lt;/li&gt;
&lt;li&gt;runs the &lt;code&gt;index.ts&lt;/code&gt; file&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;h2&gt;Add in some boilerplace code&lt;/h2&gt;
&lt;ol&gt;
&lt;li&gt;Create a &lt;code&gt;src&lt;/code&gt; directory for the source files:&lt;/li&gt;
&lt;/ol&gt;
&lt;pre class=&quot;language-go&quot; tabindex=&quot;0&quot;&gt;&lt;code class=&quot;language-go&quot;&gt;mkdir src
cd src&lt;/code&gt;&lt;/pre&gt;
&lt;ol start=&quot;2&quot;&gt;
&lt;li&gt;Create an &lt;code&gt;index.ts&lt;/code&gt; file:&lt;/li&gt;
&lt;/ol&gt;
&lt;pre class=&quot;language-go&quot; tabindex=&quot;0&quot;&gt;&lt;code class=&quot;language-go&quot;&gt;mkdir src
touch &lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;token operator&quot;&gt;/&lt;/span&gt;src&lt;span class=&quot;token operator&quot;&gt;/&lt;/span&gt;index&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;ts&lt;/code&gt;&lt;/pre&gt;
&lt;ol start=&quot;3&quot;&gt;
&lt;li&gt;Add a basic &#39;hello, world&#39; function to &lt;code&gt;index.ts&lt;/code&gt;:&lt;/li&gt;
&lt;/ol&gt;
&lt;pre class=&quot;language-go&quot; tabindex=&quot;0&quot;&gt;&lt;code class=&quot;language-go&quot;&gt;function &lt;span class=&quot;token function&quot;&gt;helloWorld&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;:&lt;/span&gt; void &lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt;
  console&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;token function&quot;&gt;log&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token string&quot;&gt;`Hello, ${process.env.GREETING}!`&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;
&lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;
<p>&lt;span class=&quot;token function&quot;&gt;helloWorld&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;
&lt;h2&gt;Run the Node process&lt;/h2&gt;
&lt;p&gt;The Node process can be run with &lt;code&gt;npm run dev&lt;/code&gt;&lt;/p&gt;
&lt;p&gt;This picks up the script that was setup in the &lt;code&gt;package.json&lt;/code&gt; file earlier - if everything's configued correctly you'll see the process fire up and run the &lt;code&gt;helloWorld&lt;/code&gt; function:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;npm run dev</p>
<p>&amp;gt; template-node-project@0.0.1 dev
&amp;gt; node --env-file=.env --watch -r ts-node/register src/index.ts</p>
<p>(node:98297) ExperimentalWarning: Watch mode is an experimental feature and might change at any time
(Use <code>node --trace-warnings ...</code> to show where the warning was created)
Hello, world!
Completed running 'src/index.ts'
&lt;/code&gt;&lt;/pre&gt;
</content>
</entry></p>
  <entry>
    <title>Enjoy the ride</title>
    <link href="https://tp-11ty.onrender.com/blog/2024-02-10-enjoy-the-ride/"/>
    <updated>2024-02-10T00:00:00Z</updated>
    <id>https://tp-11ty.onrender.com/blog/2024-02-10-enjoy-the-ride/</id>
    <content type="html">&lt;p&gt;In research that should surprise absolutely no-one who’s ever been involved in software development, &lt;a href=&quot;https://visualstudiomagazine.com/articles/2024/01/25/copilot-research.aspx&quot;&gt;there’s evidence&lt;/a&gt; that generative AI tools are &lt;a href=&quot;https://www.gitclear.com/coding_on_copilot_data_shows_ais_downward_pressure_on_code_quality&quot;&gt;strongly-correlated with more copy/paste, less code refactoring and reuse, and higher code churn.&lt;/a&gt;&lt;/p&gt;
&lt;p&gt;In other words, faster output, high volatility and lower long-term quality.&lt;/p&gt;
&lt;p&gt;If I was going to extrapolate this to the industry as a whole, I’d expect a drop in demand for experienced software developers over the next couple of years as companies go all-in on the apparent savings...&lt;/p&gt;
&lt;p&gt;...followed by an explosion in demand for people who can sort out the mess that was caused…&lt;/p&gt;
&lt;p&gt;...and an absolute shortage of those skills thanks to the hollowing-out of the workforce while AI is the apparent answer.&lt;/p&gt;
&lt;p&gt;Enjoy the ride!&lt;/p&gt;
</content>
  </entry>
</feed>