<?xml version="1.0" encoding="utf-8" standalone="yes"?>
<rss xmlns:dc="http://purl.org/dc/elements/1.1/" xmlns:content="http://purl.org/rss/1.0/modules/content/" xmlns:atom="http://www.w3.org/2005/Atom" version="2.0">
  <channel>
    <title>Will Webberley&#39;s Blog</title>
    <link>https://wilw.dev/</link>
    <description>A collection of all of my blog posts.</description>
    <image>
      <url>https://wilw.dev/media/icon.png</url>
      <link>https://wilw.dev/</link>
      <title>Will Webberley&#39;s Blog</title>
    </image>
    <generator>Hugo -- gohugo.io</generator>
    <language>en-gb</language>
    <managingEditor>blog@wilw.dev (Will Webberley)</managingEditor>
    <webMaster>blog@wilw.dev (Will Webberley)</webMaster>
    
    <lastBuildDate>Mon, 18 Dec 2023 18:47:00 +0000</lastBuildDate><atom:link href="https://wilw.dev/rss.xml" rel="self" type="application/rss+xml" />
    <item>
      <title>Cross-publishing web content to Gemini</title>
      <link>https://wilw.dev/blog/2023/06/01/automatic-gemini-publishing/</link>
      <pubDate>Thu, 01 Jun 2023 21:38:00 +0000</pubDate>
      
      <guid>https://wilw.dev/blog/2023/06/01/automatic-gemini-publishing/</guid>
      
        <category>technology</category>
      
        <category>gemini</category>
      
      
        <enclosure url="https://wilw.dev/media/blog/header-automatic-gemini-publishing.png" type="image/png"/>
      
      <content:encoded>&lt;p&gt;My personal website is generated using &lt;a href=&#34;https://gohugo.io&#34; target=&#34;_blank&#34; rel=&#34;noopener noreferrer&#34; &gt;Hugo&lt;/a&gt;, which allows me to write nearly all of the actual content itself in plain markdown files.&lt;/p&gt;
&lt;p&gt;I also maintain a Gemini capsule (hosted at &lt;a href=&#34;gemini://wilw.capsule.town&#34;&gt;gemini://wilw.capsule.town&lt;/a&gt;). For a while I’ve wanted to be able to add more content to this capsule, and to try and keep it updated more consistently over time. However, I don’t really have the capacity to duplicate the time taken to maintain the site (and its &lt;a href=&#34;https://wilw.dev/blog&#34;&gt;blog posts&lt;/a&gt; and &lt;a href=&#34;https://wilw.dev/notes&#34;&gt;notes&lt;/a&gt;) in order to do so.&lt;/p&gt;
&lt;p&gt;Whilst Gemini text format is &lt;em&gt;&lt;a href=&#34;https://gemini.circumlunar.space/docs/gemtext.gmi&#34; target=&#34;_blank&#34; rel=&#34;noopener noreferrer&#34; &gt;not markdown&lt;/a&gt;&lt;/em&gt;, it is similar enough that I wondered if I could script a simple translation flow to turn my &lt;code&gt;*.markdown&lt;/code&gt; files into valid &lt;code&gt;.gmi&lt;/code&gt; ones.&lt;/p&gt;
&lt;p&gt;At the end of the day, it is up to the Gemini clients themselves to decide how to render content, and so the main thing I really needed to handle were links (which can’t be rendered in-line in Gemini) and images (which cannot be displayed).&lt;/p&gt;
&lt;p&gt;Using the &lt;a href=&#34;https://pypi.org/project/python-frontmatter&#34; target=&#34;_blank&#34; rel=&#34;noopener noreferrer&#34; &gt;&lt;code&gt;python-frontmatter&lt;/code&gt;&lt;/a&gt; package, I was able to write a &lt;a href=&#34;https://git.wilw.dev/wilw/wilw.dev/src/branch/main/gemini/process_capsule.py&#34; target=&#34;_blank&#34; rel=&#34;noopener noreferrer&#34; &gt;quick builder script&lt;/a&gt; which now helps to populate my Gemini capsule.&lt;/p&gt;
&lt;p&gt;The script creates new Gemini files for each of my blog posts and notes, and also generates index files to allow navigation to this content. In each file, links are extracted out of the main text, and replaced with indexed references (e.g. &lt;code&gt;[3]&lt;/code&gt;), which are then placed as valid Gemini links after each paragraph.&lt;/p&gt;
&lt;p&gt;If an article has links referencing another blog post or note, then the links are generated using the &lt;code&gt;gemini://&lt;/code&gt; protocol, allowing the visitor to stay in Gemini as much as possible.&lt;/p&gt;
&lt;p&gt;The script is executed as part of my &lt;a href=&#34;https://wilw.dev/blog/2023/04/23/woodpecker-ci&#34;&gt;Woodpecker CI pipeline&lt;/a&gt; to automate the generation of the Gemini capsule on each &lt;code&gt;git push&lt;/code&gt;. The output is then automatically deployed to &lt;a href=&#34;gemini://capsule.town&#34;&gt;gemini://capsule.town&lt;/a&gt; in order to make the changes live.&lt;/p&gt;
&lt;p&gt;It’s great knowing that I can now keep the capsule up-to-date as part of my usual workflow.&lt;/p&gt;
</content:encoded>
    </item>
    
    <item>
      <title>CI/CD with Woodpecker and Gitea</title>
      <link>https://wilw.dev/blog/2023/04/23/woodpecker-ci/</link>
      <pubDate>Sun, 23 Apr 2023 16:51:00 +0000</pubDate>
      
      <guid>https://wilw.dev/blog/2023/04/23/woodpecker-ci/</guid>
      
        <category>technology</category>
      
        <category>selfhost</category>
      
      
        <enclosure url="https://wilw.dev/media/blog/header-woodpecker-gitea.png" type="image/png"/>
      
      <content:encoded>&lt;p&gt;Some of my personal projects are beginning to get larger and more complicated, often involving different front-ends and services that all need to be separately built and deployed. Managing all of these is taking away more of my personal time, and I’ve been on the look-out for a good CI/CD automation system for these projects. I primarily use &lt;a href=&#34;https://gitea.io&#34; target=&#34;_blank&#34; rel=&#34;noopener noreferrer&#34; &gt;Gitea&lt;/a&gt; as a git server, and have been struggling to find a system that suited my needs and works well with Gitea.&lt;/p&gt;
&lt;p&gt;I know of Jenkins’ &lt;a href=&#34;https://plugins.jenkins.io/gitea&#34; target=&#34;_blank&#34; rel=&#34;noopener noreferrer&#34; &gt;Gitea plugin&lt;/a&gt;, and other similar alternatives, but many of these tools are ones I’ve tried in the past and didn’t get on with for one reason or another.&lt;/p&gt;
&lt;p&gt;There is also the upcoming &lt;a href=&#34;https://blog.gitea.io/2022/12/feature-preview-gitea-actions&#34; target=&#34;_blank&#34; rel=&#34;noopener noreferrer&#34; &gt;Gitea Actions&lt;/a&gt; feature, which aims to provide similar functionality to GitHub’s own Actions. However this is still experimental and my early tests did not (yet) allow me to settle on this reliably.&lt;/p&gt;
&lt;p&gt;More recently I stumbled across &lt;a href=&#34;https://woodpecker-ci.org&#34; target=&#34;_blank&#34; rel=&#34;noopener noreferrer&#34; &gt;Woodpecker&lt;/a&gt;, which has &lt;a href=&#34;https://woodpecker-ci.org/docs/administration/vcs/gitea&#34; target=&#34;_blank&#34; rel=&#34;noopener noreferrer&#34; &gt;in-built support for Gitea&lt;/a&gt; and a super-easy setup (as I briefly document in &lt;a href=&#34;https://wilw.dev/notes/woodpecker&#34;&gt;this Woodpecker note&lt;/a&gt;).&lt;/p&gt;
&lt;p&gt;Once Woodpecker is running, one can simply login with single sign-on (SSO) provided via the configured Gitea instance. This then allows Woodpecker to automatically fetch your repos and, when attached, create the required webhooks.&lt;/p&gt;
&lt;p&gt;Adding repos to Woodpecker is straight forward through the web UI, and each repository can be configured with secrets (e.g. Docker credentials) and any extra settings. Setting up the build pipeline instructions themselves is as simple as creating a &lt;code&gt;.woodpecker.yml&lt;/code&gt; file in your project root, following the &lt;a href=&#34;https://woodpecker-ci.org/docs/usage/pipeline-syntax&#34; target=&#34;_blank&#34; rel=&#34;noopener noreferrer&#34; &gt;simple syntax&lt;/a&gt;.&lt;/p&gt;
&lt;p&gt;&lt;img src=&#34;https://wilw.dev/media/blog/woodpecker-1.png&#34; alt=&#34;A screenshot from my Woodpecker instance showing a successful build of this website&#34;&gt;&lt;/p&gt;
&lt;p&gt;I now use Woodpecker for a number of projects and services, and for a variety of tasks – including building and deploying websites/apps, Docker images, and even Flutter apps. For interest, the pipeline for this Hugo website can be &lt;a href=&#34;https://git.wilw.dev/wilw/wilw.dev/src/branch/main/.woodpecker.yml&#34; target=&#34;_blank&#34; rel=&#34;noopener noreferrer&#34; &gt;viewed here&lt;/a&gt;.&lt;/p&gt;
&lt;p&gt;From all the continuous development options I’ve tried, Woodpecker is certainly my current favourite, and I feel it is one that will stick. I find the UI simple and intuitive, and also blazing fast, with updates showing in real-time.&lt;/p&gt;
&lt;p&gt;The build speeds themselves are, of course, only as quick as the underlying resources allow. Currently I run Woodpecker on a dedicated VPS with 4GB memory, and this performs fine for my needs.&lt;/p&gt;
&lt;p&gt;🤘&lt;/p&gt;
</content:encoded>
    </item>
    
    <item>
      <title>Thoughts on book logging</title>
      <link>https://wilw.dev/blog/2023/03/24/book-logging/</link>
      <pubDate>Fri, 24 Mar 2023 18:24:00 +0000</pubDate>
      
      <guid>https://wilw.dev/blog/2023/03/24/book-logging/</guid>
      
        <category>book</category>
      
        <category>life</category>
      
      
        <enclosure url="https://wilw.dev/media/blog/header-book-logging.png" type="image/png"/>
      
      <content:encoded>&lt;p&gt;I’ve been a &lt;a href=&#34;https://www.goodreads.com/user/show/22390023-will&#34; target=&#34;_blank&#34; rel=&#34;noopener noreferrer&#34; &gt;member of Goodreads&lt;/a&gt; since 2013. I follow a few of my friends and family on there, and whilst it was nice to see the types of things people are reading, I only really ever used the service as a way for logging what I had read. The other social aspects didn’t keep me coming back and I personally didn’t find the home feed interesting.&lt;/p&gt;
&lt;p&gt;As I started to get more into the Fediverse back in 2021, I joined &lt;a href=&#34;https://joinbookwyrm.com&#34; target=&#34;_blank&#34; rel=&#34;noopener noreferrer&#34; &gt;BookWyrm&lt;/a&gt; (as &lt;a href=&#34;https://bookwyrm.social/user/wilw&#34; target=&#34;_blank&#34; rel=&#34;noopener noreferrer&#34; &gt;@wilw@bookwyrm.social&lt;/a&gt;). I was able to export my reads as a CSV from Goodreads and import them into BookWyrm, which I continued to use as a method for recording reads and listens. BookWyrm is excellent, easy-to-use, and less clunky than Goodreads. I can certainly recommend it if you’re looking to join or build a community around books.&lt;/p&gt;
&lt;p&gt;However, as with Goodreads, I just never really seemed to get involved with the other features or engaged with people on the platform. Like most Fediverse services, BookWyrm is primarily designed to be &lt;em&gt;social&lt;/em&gt;. People discuss books, write reviews for others, and get inspiration from the people in their community.&lt;/p&gt;
&lt;p&gt;I recently came to the realisation that reading doesn’t feel like a social activity to me. One of the great things about books is that they are inherently personal – fiction provides an escape from reality and evokes your own imagination, and other works can (among other things) help to educate and improve your life.&lt;/p&gt;
&lt;p&gt;I do enjoy discussing books with others from time to time (and usually in person) but, for now, all I really needed was a simple method for logging reads and listens.&lt;/p&gt;
&lt;p&gt;As such, I now simply include a log on my &lt;a href=&#34;https://wilw.dev/notes/books&#34;&gt;books note&lt;/a&gt;. There’s not much to it, but it helps me keep track of what I am reading and the frequency. To create the log, I include a CSV section in the note (which I can easily add to) and I use a shortcode (based on the very helpful &lt;a href=&#34;https://randomgeekery.org/post/2020/06/csv-and-data-tables-in-hugo&#34; target=&#34;_blank&#34; rel=&#34;noopener noreferrer&#34; &gt;work by Brian Wisti&lt;/a&gt;) to get Hugo to render a table from the CSV content.&lt;/p&gt;
</content:encoded>
    </item>
    
    <item>
      <title>A question about encryption for self-hosting</title>
      <link>https://wilw.dev/blog/2023/03/05/self-host-encryption/</link>
      <pubDate>Sun, 05 Mar 2023 09:25:00 +0000</pubDate>
      
      <guid>https://wilw.dev/blog/2023/03/05/self-host-encryption/</guid>
      
        <category>technology</category>
      
        <category>selfhost</category>
      
      
        <enclosure url="https://wilw.dev/media/blog/header-server-theft.png" type="image/png"/>
      
      <content:encoded>&lt;p&gt;I self-host a number of services - &lt;a href=&#34;https://nextcloud.com&#34; target=&#34;_blank&#34; rel=&#34;noopener noreferrer&#34; &gt;Nextcloud&lt;/a&gt;, &lt;a href=&#34;https://www.freshrss.org&#34; target=&#34;_blank&#34; rel=&#34;noopener noreferrer&#34; &gt;FreshRSS&lt;/a&gt;, &lt;a href=&#34;https://www.photoprism.app&#34; target=&#34;_blank&#34; rel=&#34;noopener noreferrer&#34; &gt;PhotoPrism&lt;/a&gt; etc. - at home on a Raspberry Pi. Attached to this Pi is a large SSD to hold the service data and configuration, and all of this is periodically backed-up via Restic to a remote site.&lt;/p&gt;
&lt;p&gt;The SSD is simply formatted with &lt;code&gt;ext4&lt;/code&gt;, and the directory containing all of these services and data is currently encrypted using &lt;a href=&#34;https://wiki.archlinux.org/title/Fscrypt&#34; target=&#34;_blank&#34; rel=&#34;noopener noreferrer&#34; &gt;fscrypt&lt;/a&gt;. I (mainly) want to encrypt the data in order to protect against the case of break in and theft in my house, however likely or unlikely that is to occur.&lt;/p&gt;
&lt;p&gt;However, the downside of this mechanism is that each time the power drops in the house (a fairly frequent event these days with constant building work going on), the directory is unable to auto-decrypt and so the services do not launch properly until I manually unlock the directory using a passkey.&lt;/p&gt;
&lt;p&gt;Whilst this works fine, the approach feels a bit clunky. I wondered if anyone had any alternative solutions to managing this. I’m certainly not an expert in encryption mechanisms, but below I outline three approaches I’ve wondered about.&lt;/p&gt;
&lt;h2 id=&#34;option-1-fscrypt-pam-decryption&#34;&gt;Option 1: fscrypt PAM decryption&lt;/h2&gt;
&lt;p&gt;I notice from the &lt;a href=&#34;https://wiki.archlinux.org/title/Fscrypt&#34; target=&#34;_blank&#34; rel=&#34;noopener noreferrer&#34; &gt;documentation&lt;/a&gt; that directories can be automatically unlocked at login-time through PAM. Is there a secure way to auto-login a user after boot (and drive-mounting) such that the directory is ready by the time the Docker daemon restarts the service containers?&lt;/p&gt;
&lt;h2 id=&#34;option-2-full-disk-encryption-with-luks&#34;&gt;Option 2: Full-disk encryption with LUKS&lt;/h2&gt;
&lt;p&gt;In the past, I’ve used &lt;a href=&#34;https://en.wikipedia.org/wiki/Linux_Unified_Key_Setup&#34; target=&#34;_blank&#34; rel=&#34;noopener noreferrer&#34; &gt;LUKS&lt;/a&gt; to encrypt entire volumes, and &lt;code&gt;crypttab&lt;/code&gt; to handle the unlock automatically at boot-time. I could use this to encrypt the entire volume on the SSD.&lt;/p&gt;
&lt;p&gt;My concern with this is that (in my own way of thinking about it!) the encryption key must be stored unencrypted on the root/boot filesystem in order to facilitate the automatic unlock. If the device was to be stolen, this key would be available to anyone checking the filesystem. I may be misunderstanding the encryption approach here, or perhaps over-estimating an average thief’s technical ability.&lt;/p&gt;
&lt;h2 id=&#34;option-3-resistance-to-power-drops&#34;&gt;Option 3: Resistance to power drops&lt;/h2&gt;
&lt;p&gt;My final thought on this is to address the root cause of the issue, and to find a solution that keeps the Pi booted when there is no mains power. For example, this could be accomplished with a UPS (Uninterruptible Power Supply).&lt;/p&gt;
&lt;h2 id=&#34;my-ask&#34;&gt;My ask&lt;/h2&gt;
&lt;p&gt;I understand that this must not be a rare problem. If you’ve had similar experiences and have found a solution that works well for you, I’d love to hear from you!&lt;/p&gt;
&lt;p&gt;Please reach out to me on Mastodon (&lt;a href=&#34;https://fosstodon.org/@wilw&#34; target=&#34;_blank&#34; rel=&#34;noopener noreferrer&#34; &gt;@wilw@fosstodon.org&lt;/a&gt;) or via email (&lt;a href=&#34;mailto:blog@wilw.dev&#34;&gt;blog@wilw.dev&lt;/a&gt;).&lt;/p&gt;
</content:encoded>
    </item>
    
    <item>
      <title>Window management workflows on macOS</title>
      <link>https://wilw.dev/blog/2022/12/10/macos-windows/</link>
      <pubDate>Sat, 10 Dec 2022 15:40:00 +0000</pubDate>
      
      <guid>https://wilw.dev/blog/2022/12/10/macos-windows/</guid>
      
        <category>technology</category>
      
      
        <enclosure url="https://wilw.dev/media/blog/header-mac-windows.png" type="image/png"/>
      
      <content:encoded>&lt;h2 id=&#34;macos-window-management&#34;&gt;macOS window management&lt;/h2&gt;
&lt;p&gt;Every Mac user seems to have a different way of managing their open applications and windows.&lt;/p&gt;
&lt;p&gt;Some people prefer to view each window in “full” mode, in which they take up the entire display and the user can cycle apps or use the dock to change the active window. Other people use full-screen mode and/or swipe between desktop spaces to find their apps, or a mixture of several approaches.&lt;/p&gt;
&lt;p&gt;Personally, I only tend to use one desktop workspace and I keep all my application windows open, which I click or cycle between with &lt;code&gt;⌘-tab&lt;/code&gt; (or &lt;code&gt;⌘-`&lt;/code&gt; for windows in a single application). At any one time, I usually (at least) have open Mail, Calendar, Spotify, Firefox, Nova, Slack, Teams, and Sorted. And although I do minimise windows (to the dock) occasionally, things can get messy very quickly.&lt;/p&gt;
&lt;p&gt;Some may think I’m crazy, but I’ve lived like this for over a decade now, and things have been mostly fine so far. However, I’ve recently noticed that app windows behind my currently-active one can get distracting (for example, new Slack messages, or emails arriving), causing me to lose focus on my current task.&lt;/p&gt;
&lt;p&gt;The visual complexity of the screen may also be having a toll on my ability to concentrate or find the things I’m looking for, especially since the visual hierarchy of the current window is interrupted by everything else going on around it.&lt;/p&gt;
&lt;h2 id=&#34;the-window-hiding-workflow&#34;&gt;The window-hiding workflow&lt;/h2&gt;
&lt;p&gt;These factors weren’t anything I was particularly concerned about, but a &lt;a href=&#34;https://atp.fm/503&#34; target=&#34;_blank&#34; rel=&#34;noopener noreferrer&#34; &gt;semi-recent episode&lt;/a&gt; of the &lt;a href=&#34;https://atp.fm&#34; target=&#34;_blank&#34; rel=&#34;noopener noreferrer&#34; &gt;Accidental Tech Podcast&lt;/a&gt;, which I frequently listen to, talked about this exact problem. The hosts discussed the concept of “hiding” windows as part of their normal workflow.&lt;/p&gt;
&lt;p&gt;Window-hiding in itself isn’t a new concept to me, and I even knew about the &lt;code&gt;cmd-H&lt;/code&gt; keybinding before hearing the episode. However, listening to how people use this feature as part of their usual workflows really interested me.&lt;/p&gt;
&lt;p&gt;The general idea for this type of workflow is that, once a task is completed (e.g. choosing a new Spotify playlist or sending an email), one “hides” the app until it is needed again. This can be achieved by using the &lt;code&gt;cmd-H&lt;/code&gt; keybinding (which hides the app and automatically switches the focus to the next-most recent app in the stack) or option-clicking another window (which leaves and hides the current app).&lt;/p&gt;
&lt;p&gt;The effect of this workflow is that one only has a single (or very few) number of application windows showing at any one time. When you need to re-show a hidden app, you can just cycle to it in the app-switcher or select it from the dock.&lt;/p&gt;
&lt;p&gt;I’ve been using this workflow now for a few weeks, and have noticed a big improvement in my focus on tasks that, when combined with an effective to-do list system, has had a big positive hit on my work effectiveness.&lt;/p&gt;
&lt;p&gt;Earlier in the year &lt;a href=&#34;https://wilw.dev/blog/2022/06/12/ipad-coding&#34;&gt;I blogged&lt;/a&gt; about how I spend more time working on iPads these days, which is partially down to a preference of the single-screen context afforded by iPadOS. I can certainly see why I enjoy replicating this process on macOS too.&lt;/p&gt;
&lt;h2 id=&#34;addendum-stage-manager&#34;&gt;Addendum: Stage Manager&lt;/h2&gt;
&lt;p&gt;&lt;a href=&#34;https://support.apple.com/en-us/HT213315&#34; target=&#34;_blank&#34; rel=&#34;noopener noreferrer&#34; &gt;Stage Manager&lt;/a&gt; was recently released along with macOS Ventura. I haven’t given this a proper go yet, but I plan to soon since it looks like workflows based on this could also help boost productivity and focus.&lt;/p&gt;
</content:encoded>
    </item>
    
    <item>
      <title>From Google and Apple Photos to Photoprism</title>
      <link>https://wilw.dev/blog/2022/11/25/photoprism/</link>
      <pubDate>Fri, 25 Nov 2022 19:35:00 +0000</pubDate>
      
      <guid>https://wilw.dev/blog/2022/11/25/photoprism/</guid>
      
        <category>technology</category>
      
        <category>selfhost</category>
      
      
        <enclosure url="https://wilw.dev/media/blog/header-photos-apps.png" type="image/png"/>
      
      <content:encoded>&lt;h2 id=&#34;a-history-of-media-storage-solutions&#34;&gt;A history of media storage solutions&lt;/h2&gt;
&lt;p&gt;Back in 2021 &lt;a href=&#34;https://wilw.dev/blog/2021/02/24/google-photos-pcloud&#34;&gt;I blogged&lt;/a&gt; about how and why I wanted to switch from Google Photos as a storage solution (and source of truth) for my life’s photo and video library. The post compared several solutions, such as Piwigo, Mega, and Nextcloud.&lt;/p&gt;
&lt;p&gt;In that time I’ve tried several further options, starting with &lt;a href=&#34;https://www.pcloud.com&#34; target=&#34;_blank&#34; rel=&#34;noopener noreferrer&#34; &gt;pCloud&lt;/a&gt; (as described in that post), &lt;a href=&#34;https://wilw.dev/blog/2021/12/11/nextcloud-object-storage&#34;&gt;Nextcloud backed by S3&lt;/a&gt;, and plain-old Apple Photos.&lt;/p&gt;
&lt;p&gt;The primary way by which I capture photos is through my phone’s camera, and my main issue with pCloud was the reliability of its mobile app photo auto-uploader. It would often get logged-out and miss several days or even weeks of sync, without any notification. After logging-in again, the app offered no way to catch-up specifically on the missed syncs, and so it would involve having to manually fill-in the gaps myself.&lt;/p&gt;
&lt;p&gt;&lt;a href=&#34;https://nextcloud.com&#34; target=&#34;_blank&#34; rel=&#34;noopener noreferrer&#34; &gt;Nextcloud&lt;/a&gt; is an excellent option for self-hosting files, and I make strong use out of this myself. However, I found it a bit slow and clunky for managing images and videos. It would often grind slowly to the point of unresponsiveness whilst it performed background tasks - I imagine related to indexing or creating thumbnail previews of media content. Its image viewer also isn’t as useful as other systems.&lt;/p&gt;
&lt;p&gt;I was sinking a lot of time into this, and so in the end I gave up and resorted to simply using Apple Photos - it’s easy, cost-effective, and naturally works well across all of my Apple devices.&lt;/p&gt;
&lt;h2 id=&#34;apple-photos&#34;&gt;Apple Photos&lt;/h2&gt;
&lt;p&gt;However, I couldn’t sit still, and I was constantly worried about the single point of failure in having everything stored in a single “basket”. What would happen if I lost access to my Apple account? How can I make sure things are backed-up? What if I chose to move away from the Apple ecosystem?&lt;/p&gt;
&lt;p&gt;For a short while I made use of &lt;a href=&#34;https://www.backblaze.com/mac-online-backup.html&#34; target=&#34;_blank&#34; rel=&#34;noopener noreferrer&#34; &gt;Backblaze’s Mac backup&lt;/a&gt; solution to try to backup my photo library on my Mac. However, despite having configured the Photos app on my Mac to download the full versions of everything in the library, the file size differences in the photo library file on my Mac vs. that reported by iCloud on the Mac (and the storage settings panes on my iPhone) just didn’t add up. I also recently &lt;a href=&#34;https://wilw.dev/blog/2022/10/02/bear-joplin&#34;&gt;had similar issues&lt;/a&gt; with the opaqueness of iCloud in a slightly different context.&lt;/p&gt;
&lt;p&gt;Either way, I was worried I was missing something and that the backups would not be complete. Also, the solution depended on my Mac being awake and receiving the photo sync in order for its local library to be successfully backed-up with all of the recent changes.&lt;/p&gt;
&lt;h2 id=&#34;photoprism&#34;&gt;Photoprism&lt;/h2&gt;
&lt;p&gt;More and more I’ve heard talk on the grapevine about alternative self-hostable options for photos and media. Some of these can reportedly perform super well, including the Google Photos-like &lt;a href=&#34;https://github.com/immich-app/immich&#34; target=&#34;_blank&#34; rel=&#34;noopener noreferrer&#34; &gt;Immich&lt;/a&gt;.&lt;/p&gt;
&lt;p&gt;&lt;a href=&#34;https://photoprism.app&#34; target=&#34;_blank&#34; rel=&#34;noopener noreferrer&#34; &gt;Photoprism&lt;/a&gt; is another option that had come up a few times, and I resonated well with its feature list. It supports face and object recognition, automatic categorisation, favourites, moments, albums, and much more. It also has a rich suite of settings and advanced import capabilities.&lt;/p&gt;
&lt;p&gt;The disadvantage with self-hosting a service like this is the amount of storage required. Images and videos aren’t small files, particularly when taking into account the quality produced by modern smartphones. I alluded to this back in my blog post from last year, but since then my mindset has shifted much more to prioritising data sovereignty over ease of use and surrendering privacy.&lt;/p&gt;
&lt;p&gt;I deployed Photoprism and documented my setup &lt;a href=&#34;https://wilw.dev/notes/photoprism&#34;&gt;in this note&lt;/a&gt;. I’ve now been using it successfully for a few months and have really enjoyed it. My library backs-up nicely with &lt;a href=&#34;https://restic.net&#34; target=&#34;_blank&#34; rel=&#34;noopener noreferrer&#34; &gt;Restic&lt;/a&gt; to &lt;a href=&#34;https://www.backblaze.com/cloud-storage&#34; target=&#34;_blank&#34; rel=&#34;noopener noreferrer&#34; &gt;Backblaze B2&lt;/a&gt; and everything feels smooth and safe. The app is only available via my Tailscale network for extra security and the storage volume &lt;a href=&#34;https://wilw.dev/notes/volume-encryption&#34;&gt;is encrypted&lt;/a&gt;.&lt;/p&gt;
&lt;p&gt;&lt;img src=&#34;https://wilw.dev/media/blog/photoprism1.png&#34; alt=&#34;A screenshot showing a folder in my Photoprism library&#34;&gt;&lt;/p&gt;
&lt;p&gt;My library is over 200GB big, and so I needed a hefty volume attached to my VPS. I also wanted a machine powerful enough to process the images and videos, and to ensure things didn’t become too slow when scrolling through thousands of images. As such, deploying a service like this is certainly not the cheapest option, but it is one that works well for me.&lt;/p&gt;
&lt;p&gt;For automatic photo sync from my iPhone I use the &lt;a href=&#34;https://www.photosync-app.com/home.html&#34; target=&#34;_blank&#34; rel=&#34;noopener noreferrer&#34; &gt;Photosync&lt;/a&gt; app. This has been reliable and I can certainly recommend it. I’ve not had a single upload failure during the months I’ve been using the app. My setup for this is also included in that &lt;a href=&#34;https://wilw.dev/notes/photoprism&#34;&gt;same note&lt;/a&gt;.&lt;/p&gt;
&lt;p&gt;Photosync also supports pCloud as a sync target, but having tried Photoprism I haven’t looked back - its experience is far superior.&lt;/p&gt;
&lt;p&gt;&lt;img src=&#34;https://wilw.dev/media/blog/photoprism2.png&#34; alt=&#34;A screenshot showing my Photoprism library&amp;rsquo;s &amp;ldquo;map&amp;rdquo; view&#34;&gt;&lt;/p&gt;
&lt;p&gt;So far, this has been my most positive photo management experience, and I am really happy with the setup. To me, it is more fully-featured and much more performant than Google Photos. I saved the Photoprism PWA to my phone’s homescreen for quick access to my photos.&lt;/p&gt;
&lt;p&gt;Here are a few of the other features I enjoy about Photoprism:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;Speed: I find it super fast to use and navigate around. Images and videos load quickly. The webapp loads more quickly than the native Apple Photos app does.&lt;/li&gt;
&lt;li&gt;Apple live photos are supported out-of-the-box and the UI for viewing these is better than in Apple Photos.&lt;/li&gt;
&lt;li&gt;The libary review process is great for ensuring that only the photos I actually want to keep make it into my library. Photoprism is good at detecting the types of files that should be reviewed.&lt;/li&gt;
&lt;li&gt;Photos marked as “favourites” on Apple Photos are automatically reflected into the Favourites album on Photoprism.&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;Having the images stored safely on a volume I have direct access to means I can pretty easily switch to a new self-hosted app if I wanted to without a tedious export from a commercial system. I don’t know if this is the end of the road (yet) for my photo management solution adventure, but it’s definitely in a good place for now!&lt;/p&gt;
</content:encoded>
    </item>
    
    <item>
      <title>Tailscale: multi-service HTTPS on a single machine</title>
      <link>https://wilw.dev/blog/2022/10/27/tailscale-virtualhosts/</link>
      <pubDate>Thu, 27 Oct 2022 18:48:00 +0000</pubDate>
      
      <guid>https://wilw.dev/blog/2022/10/27/tailscale-virtualhosts/</guid>
      
        <category>technology</category>
      
        <category>selfhost</category>
      
      
        <enclosure url="https://wilw.dev/media/blog/header-secure-network.png" type="image/png"/>
      
      <content:encoded>&lt;h2 id=&#34;https-on-tailscale&#34;&gt;HTTPS on Tailscale&lt;/h2&gt;
&lt;p&gt;&lt;a href=&#34;https://www.tailscale.com&#34; target=&#34;_blank&#34; rel=&#34;noopener noreferrer&#34; &gt;Tailscale&lt;/a&gt;’s &lt;a href=&#34;https://tailscale.com/kb/1153/enabling-https&#34; target=&#34;_blank&#34; rel=&#34;noopener noreferrer&#34; &gt;HTTPS&lt;/a&gt; feature is an excellent tool for adding TLS connections to web services exposed over the tailnet.&lt;/p&gt;
&lt;p&gt;Although traffic over the tailnet is encrypted anyway due to the nature of Tailscale itself, some web-based services work better when served over HTTPS. Since the browser does not know that you are accessing the service over a secure connection, it may enforce limits on connected web services when accessing them in - what feels like - an insecure context.&lt;/p&gt;
&lt;p&gt;Enabling HTTPS for Tailscale machines is as simple as &lt;a href=&#34;https://tailscale.com/kb/1153/enabling-https&#34; target=&#34;_blank&#34; rel=&#34;noopener noreferrer&#34; &gt;running &lt;code&gt;tailscale cert&lt;/code&gt;&lt;/a&gt; on each machine you want to issue a certificate for. This command will cause Tailscale to request a TLS certificate using Let’s Encrypt for the machine and to write the certificate and key file to the filesystem.&lt;/p&gt;
&lt;p&gt;In an ideal world, this is great, since you can then just plug these files into your reverse-proxy in order to add HTTPS capability to your web service.&lt;/p&gt;
&lt;h2 id=&#34;limitations-in-multi-service-contexts&#34;&gt;Limitations in multi-service contexts&lt;/h2&gt;
&lt;p&gt;However, Tailscale can only issue one certificate per machine. If your tailnet is named “my-tailnet.ts.net” and one of your machines is called “machine1”, then running the command on this machine will generate a certificate for “machine1.my-tailnet.ts.net”. Since Tailscale doesn’t (currently) support machine sub-names (e.g. for “app1.machine1.my-tailscale.ts.net”), then it becomes tricky to use a single reverse-proxy to terminate TLS for several services at once from the single machine.&lt;/p&gt;
&lt;p&gt;Reverse-proxy web servers typically support routing based on host (i.e. subdomains), path, headers, and more. But in this case, host-based routing wouldn’t work since there is only one host name for which the certificate is valid. Path-based routing can become a nightmare to manage (and many services do not expect to be served from a non-root path), and header/parameter-based routing is not accessible at all from a number of devices (e.g. mobile browsers).&lt;/p&gt;
&lt;h2 id=&#34;my-experiences&#34;&gt;My experiences&lt;/h2&gt;
&lt;p&gt;I host a number of personal services on a single VPS, which is a machine on my tailnet. Generally, I do not bother using TLS for these services, since they are happy to be served over plain old HTTP on a port I assign them, and the traffic is secured anyway by Tailscale itself.&lt;/p&gt;
&lt;p&gt;However I recently started using &lt;a href=&#34;https://github.com/Bubka/2FAuth&#34; target=&#34;_blank&#34; rel=&#34;noopener noreferrer&#34; &gt;2FAuth&lt;/a&gt; as a self-hosted alternative to &lt;a href=&#34;https://authy.com&#34; target=&#34;_blank&#34; rel=&#34;noopener noreferrer&#34; &gt;Authy&lt;/a&gt;, and while 95% of it works fine using my normal pattern, browsers block the camera (for scanning QR codes) when working in a non-HTTPS context. Since this is (for me, at least) a crucial part of the 2FA setup experience, this became a major blocker.&lt;/p&gt;
&lt;p&gt;I do not want to expose this service outside of my tailnet and so I needed to find a method for serving it over HTTPS using the Tailscale certificate approach.&lt;/p&gt;
&lt;p&gt;One option would be to spin-up a new instance (and Tailscale machine) dedicated to the service, but this would not be worth the cost for such a small service. Alternatively, I could make this the only HTTPS-served service on the machine, in which case 2FAuth would be the sole user of the certificate issued for the entire machine. This feels like a dirty and non-scalable approach; for example, I may have other services in the future that also rely on an HTTPS connection.&lt;/p&gt;
&lt;p&gt;The solution I went with in the end depends on the fact that the browser doesn’t care which port secure traffic is served from on the machine. In particular, this means that the machine can use the same Tailscale-issued certificate to expose HTTPS traffic simultaneously on ports 8001, 8002, 54321, or anything else. In practice, this would typically involve using a deidcated reverse-proxy server for each service that requires HTTPS, and with each server being provided with the same key and certificate.&lt;/p&gt;
&lt;p&gt;To make this work in my case, I simply stuck an &lt;code&gt;nginx&lt;/code&gt; container, provided with the certificate and key, in front of the service, as &lt;a href=&#34;https://wilw.dev/notes/2fauth&#34;&gt;documented in this note&lt;/a&gt;. For another service, I would use an additional &lt;code&gt;nginx&lt;/code&gt; container, mount the &lt;em&gt;same&lt;/em&gt; certificate and key, and then that service would also be served over HTTPS - just on a different port.&lt;/p&gt;
&lt;p&gt;To keep things neat and network-separated, I would include the Nginx container definition in the same &lt;code&gt;docker-compose.yml&lt;/code&gt; file as the service itself, as also &lt;a href=&#34;https://wilw.dev/notes/2fauth&#34;&gt;documented in the same note&lt;/a&gt;.&lt;/p&gt;
&lt;h2 id=&#34;conclusion&#34;&gt;Conclusion&lt;/h2&gt;
&lt;p&gt;I don’t know if this is the most elegant or resource-effective solution, but it is one that works for me for now. I hope that, eventually, Tailscale will be abe to support sub-names for machines in order to improve this experience.&lt;/p&gt;
&lt;p&gt;If you’ve come across this issue yourself and found an alternative/better way to manage “virtual hosts” on a single Tailscale machine, then I’d love to hear from you.&lt;/p&gt;
</content:encoded>
    </item>
    
    <item>
      <title>Note-taking apps: Bear and Joplin</title>
      <link>https://wilw.dev/blog/2022/10/02/bear-joplin/</link>
      <pubDate>Sun, 02 Oct 2022 19:55:00 +0000</pubDate>
      
      <guid>https://wilw.dev/blog/2022/10/02/bear-joplin/</guid>
      
        <category>technology</category>
      
        <category>selfhost</category>
      
      
        <enclosure url="https://wilw.dev/media/blog/header-note-taking.png" type="image/png"/>
      
      <content:encoded>&lt;p&gt;The &lt;a href=&#34;https://bear.app&#34; target=&#34;_blank&#34; rel=&#34;noopener noreferrer&#34; &gt;Bear&lt;/a&gt; notes app has been my go-to notes app for Mac, iPhone, and iPad for some time now. It’s got a great UX, a customisable UI, and is one of those apps that feels like a (clichéd) “delight” to use.&lt;/p&gt;
&lt;h2 id=&#34;some-of-my-gripes-with-bear-and-cloudkit-in-general&#34;&gt;Some of my gripes with Bear (and CloudKit in general)&lt;/h2&gt;
&lt;p&gt;Bear is written exclusively for Apple devices, and uses &lt;a href=&#34;https://developer.apple.com/icloud/cloudkit&#34; target=&#34;_blank&#34; rel=&#34;noopener noreferrer&#34; &gt;CloudKit&lt;/a&gt; to sync notes between devices via iCloud. In theory, this isn’t too much of a problem. However, I’ve recently found CloudKit-reliant apps to become a little unreliable.&lt;/p&gt;
&lt;p&gt;Frequently, I’d start a quick note on my phone and the changes wouldn’t come through to my Mac in any reasonable time for me to continue writing. I don’t know if this is a limitation with CloudKit or a factor with the Bear app itself, but I find that many apps built on CloudKit tend to not display any kind of progress bar or confirmation that things have been successfully synced.&lt;/p&gt;
&lt;p&gt;Also, despite things being “securely stored in my iCloud account”, there seems to be no way to actually view the notes themselves. As with all CloudKit apps, they take up space in iCloud, but without any meaningful way to manage this storage.&lt;/p&gt;
&lt;p&gt;&lt;img src=&#34;https://wilw.dev/media/blog/bear1.png&#34; alt=&#34;My iCloud Drive folder&#34;&gt;&lt;/p&gt;
&lt;p&gt;iCloud preferences list it as an app using iCloud, but that’s pretty much the limit, as far as I can see.&lt;/p&gt;
&lt;p&gt;&lt;img src=&#34;https://wilw.dev/media/blog/bear2.png&#34; alt=&#34;iCloud settings listing the Bear app&#34;&gt;&lt;/p&gt;
&lt;p&gt;I’m sure this level of opacity is designed to make things easier for the user (and probably safer!), but I personally find that it makes it hard for me to reliably organise it alongside the rest of my digital life.&lt;/p&gt;
&lt;p&gt;Furthermore, in Bear, notes are categorised by &lt;em&gt;tags&lt;/em&gt; rather than into a folder-like hierarchy. Similar to the Gmail approach to using labels instead of traditional IMAP-style folders, this allows notes to sit in multiple tag “folders” at once. As such, I can imagine it is tricky for the Bear developers to represent this sensibly in a tree. Indeed, when exporting Bear notes, they are exported as a single flat directory of files.&lt;/p&gt;
&lt;p&gt;This is different to other Notes apps I’ve used in the past, such as &lt;a href=&#34;https://obsidian.md&#34; target=&#34;_blank&#34; rel=&#34;noopener noreferrer&#34; &gt;Obsidian&lt;/a&gt; and &lt;a href=&#34;https://simplenote.com&#34; target=&#34;_blank&#34; rel=&#34;noopener noreferrer&#34; &gt;SimpleNote&lt;/a&gt;, which are perfectly happy to directly use the filesystem as a data store. However, I do understand where syncing this with iOS could pose difficulties.&lt;/p&gt;
&lt;p&gt;This approach facilitates app “lock-in”, since I can only ever use Bear (and its “Pro” plan at that) in order to continue to sync and edit the notes, since exporting them loses the “folder” hierarchy.&lt;/p&gt;
&lt;p&gt;Despite all this, Bear is certainly a great app, and I have no problems recommending it to Apple product users. I just had a feeling that a different solution might work better for me.&lt;/p&gt;
&lt;h2 id=&#34;why-i-prefer-joplin&#34;&gt;Why I prefer Joplin&lt;/h2&gt;
&lt;p&gt;&lt;a href=&#34;https://joplinapp.org&#34; target=&#34;_blank&#34; rel=&#34;noopener noreferrer&#34; &gt;Joplin&lt;/a&gt; is an open-source note taking app available on a number of platforms.&lt;/p&gt;
&lt;p&gt;It’s not quite as slick as Bear, but Joplin is still quick and easy to use and it maintains notes in a sensible hierarchy (divided into “notebooks”), whilst also supporting tagging. It is also much more configurable, with lots of app styles, plugins, and more. Exporting Joplin notes using the app also maintains the folder structure.&lt;/p&gt;
&lt;p&gt;&lt;img src=&#34;https://wilw.dev/media/blog/bear3.png&#34; alt=&#34;Joplin app showing settings pane&#34;&gt;&lt;/p&gt;
&lt;p&gt;Joplin supports a number of sync targets, such as &lt;a href=&#34;https://nextcloud.com&#34; target=&#34;_blank&#34; rel=&#34;noopener noreferrer&#34; &gt;Nextcloud&lt;/a&gt;, WebDav, and S3. It also supports Joplin Server - a self-hosted server option. This is what I use and I’ve &lt;a href=&#34;https://wilw.dev/notes/joplin&#34;&gt;noted my setup&lt;/a&gt;. I access and sync my notes securely via &lt;a href=&#34;https://tailscale.com&#34; target=&#34;_blank&#34; rel=&#34;noopener noreferrer&#34; &gt;Tailscale&lt;/a&gt; and &lt;a href=&#34;https://wilw.dev/notes/backups&#34;&gt;back them up&lt;/a&gt; to &lt;a href=&#34;https://www.backblaze.com/cloud-storage&#34; target=&#34;_blank&#34; rel=&#34;noopener noreferrer&#34; &gt;Backblaze B2&lt;/a&gt;.&lt;/p&gt;
&lt;p&gt;&lt;img src=&#34;https://wilw.dev/media/blog/bear4.png&#34; alt=&#34;Joplin app sync target options&#34;&gt;&lt;/p&gt;
&lt;p&gt;The Joplin app also features a sync progress and success indicator for that extra peace of mind 😀.&lt;/p&gt;
&lt;p&gt;&lt;img src=&#34;https://wilw.dev/media/blog/bear5.png&#34; alt=&#34;Joplin sync indication&#34;&gt;&lt;/p&gt;
&lt;p&gt;Both Bear and Joplin are great note-taking experiences, but I feel safer with Joplin and it meets my current needs more.&lt;/p&gt;
</content:encoded>
    </item>
    
    <item>
      <title>Re-Building my Website with Hugo</title>
      <link>https://wilw.dev/blog/2022/09/03/re-building-my-website-with-hugo/</link>
      <pubDate>Sat, 03 Sep 2022 15:52:00 +0000</pubDate>
      
      <guid>https://wilw.dev/blog/2022/09/03/re-building-my-website-with-hugo/</guid>
      
        <category>technology</category>
      
        <category>javascript</category>
      
      
        <enclosure url="https://wilw.dev/media/blog/header-website-rewrite.png" type="image/png"/>
      
      <content:encoded>&lt;h2 id=&#34;gatsbyjs&#34;&gt;GatsbyJS&lt;/h2&gt;
&lt;p&gt;For several years I’ve been using &lt;a href=&#34;https://www.gatsbyjs.com&#34; target=&#34;_blank&#34; rel=&#34;noopener noreferrer&#34; &gt;GatsbyJS&lt;/a&gt; to generate the static site content for this website. Gatsby is a great tool and produces blazing-fast websites through the use of an interesting combination of technologies.&lt;/p&gt;
&lt;p&gt;In Gatsby, pages are simply &lt;a href=&#34;https://reactjs.org&#34; target=&#34;_blank&#34; rel=&#34;noopener noreferrer&#34; &gt;React&lt;/a&gt; components, and developers can make use of the entire JavaScript and React ecosystems to craft their sites. Config files can be used to create pages that don’t “exist” in your filesystem (e.g. an index page for each tag used in a blog) and &lt;a href=&#34;https://graphql.org&#34; target=&#34;_blank&#34; rel=&#34;noopener noreferrer&#34; &gt;GraphQL&lt;/a&gt; queries are used to surface content and query data from across the website. Gatsby templates and standard React composition patterns allow for excellent re-use of components.&lt;/p&gt;
&lt;p&gt;When building the site, Gatsby produces standard HTML files for each of your pages, such that they can (in a limited sense) be accessed by JavaScript-less browsers. When JavaScript is enabled on the browser, Gatsby pre-fetches content automatically in the background and routes requests internally to make navigating through the pages extremely quick.&lt;/p&gt;
&lt;p&gt;Gatsby has served me well for a number of years and I’ve written a number of posts on this blog around tweaking and enhancing setups to give even better experiences.&lt;/p&gt;
&lt;h2 id=&#34;why-consider-a-move&#34;&gt;Why consider a move?&lt;/h2&gt;
&lt;p&gt;For a small personal website, like mine, however, Gatsby felt a bit overkill. I shouldn’t need to write JavaScript to present simple static content, and dealing with the JS dependency graph and a huge default &lt;code&gt;node_modules/&lt;/code&gt; is a little like hitting a nail with a sledgehammer.&lt;/p&gt;
&lt;p&gt;When updating Gatsby or any other dependencies, I’d always end up fighting against issues with incompatibilities, taking my time away from actually producing any content. GitHub’s Dependabot alerts would email me frequently about insecure packages &lt;em&gt;somewhere&lt;/em&gt; in the graph, which I wouldn’t mind so much if I wasn’t simply needing just a small bunch of HTML files.&lt;/p&gt;
&lt;p&gt;I find the Gatsby config files hard to understand, read, and write. I’d often end up blindly copying bits from other examples without really understanding what they were doing. The same applies to the GraphQL queries; I never really got my head around these in the Gatsby context, and I ended up reducing the site’s functionality because I couldn’t achieve what I wanted to. Sure - I could spend more time learning about these concepts but I just couldn’t bring myself to do so when my target needs of the system are so simplistic.&lt;/p&gt;
&lt;p&gt;As the site got bigger I found that the build times also increased. Towards the end of my usage, the site would take 20-30 seconds to boot the hot-reloading &lt;code&gt;develop&lt;/code&gt; mode, and building the production-ready site would often take a good minute or more. I use Vercel to host the site, and so I imagine it has to run a &lt;code&gt;yarn install&lt;/code&gt; each time it builds too.&lt;/p&gt;
&lt;p&gt;When in production, the site’s assets - whilst not &lt;em&gt;huge&lt;/em&gt; - would be pretty sizeable given the actual content being displayed. Visitors shouldn’t need to load a React runtime just to view the homepage (though I do understand that the JavaScript components are there to improve the quality of the UX).&lt;/p&gt;
&lt;p&gt;All in all, I felt that Gatsby was too big/heavy/complex for my needs, and with too many overheads in terms of up-keep and maintenance.&lt;/p&gt;
&lt;h2 id=&#34;switching-to-hugo&#34;&gt;Switching to Hugo&lt;/h2&gt;
&lt;p&gt;&lt;a href=&#34;https://gohugo.io&#34; target=&#34;_blank&#34; rel=&#34;noopener noreferrer&#34; &gt;Hugo&lt;/a&gt; has been on my radar for some time now, but I never got around to giving it a go. I understood its aims and characteristics (especially given my past experience with &lt;a href=&#34;https://jekyllrb.com&#34; target=&#34;_blank&#34; rel=&#34;noopener noreferrer&#34; &gt;Jekyll&lt;/a&gt;), and it always seemed as though it would be a good fit for my needs.&lt;/p&gt;
&lt;p&gt;Given my growing concerns about Gatsby, and having a couple of days of work downtime last month, I thought I’d give Hugo a proper try by attempting to port my personal website over to it.&lt;/p&gt;
&lt;p&gt;I began by installing the &lt;code&gt;hugo&lt;/code&gt; tool and creating a new site &lt;a href=&#34;https://gohugo.io/getting-started/quick-start&#34; target=&#34;_blank&#34; rel=&#34;noopener noreferrer&#34; &gt;according to the documentation&lt;/a&gt;. I knew that Hugo content is mostly written in markdown, and since my blog posts and notes were already in markdown from their Gatsby days anyway, I could copy them straight over into the relevant directories under &lt;code&gt;content/&lt;/code&gt; in the new Hugo site.&lt;/p&gt;
&lt;p&gt;Although there was now content in the site, Hugo would still render blank pages. This is because I didn’t have a theme configured. I had a quick look through the &lt;a href=&#34;https://themes.gohugo.io&#34; target=&#34;_blank&#34; rel=&#34;noopener noreferrer&#34; &gt;available themes&lt;/a&gt; on the website, but none of them really took my fancy. Instead, I started to build out my own basic theme by creating HTML files in the &lt;code&gt;layouts/&lt;/code&gt; directory.&lt;/p&gt;
&lt;p&gt;My goal was to try and replicate my existing site as much as possible. Partly because this would be quicker (I wouldn’t need to come up with new designs and layouts), but also because I like its simplicity.&lt;/p&gt;
&lt;p&gt;I gradually migrated all of my content over, making use of content organisation, indexes, layout files, and simple styling. The &lt;a href=&#34;https://gohugo.io/documentation&#34; target=&#34;_blank&#34; rel=&#34;noopener noreferrer&#34; &gt;documentation&lt;/a&gt; is easy to follow and very intuitive. There’s no GraphQL to learn - it’s all just basic HTML, markdown and some simple Hugo functions.&lt;/p&gt;
&lt;p&gt;Before long, I had the whole site copied over and built, and I also thoroughly enjoyed the process. I use none of my own JavaScript (aside from a couple of pieces I include, for which I’ll justify below), and so everything feels much simpler and cleaner. I now look forward to adding more content and continuing to tweak the site going forward.&lt;/p&gt;
&lt;h2 id=&#34;things-i-love-&#34;&gt;Things I love ❤️&lt;/h2&gt;
&lt;p&gt;Here are some of the things I’ve found to love about Hugo:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;Partials&lt;/strong&gt; - like React components, these are great for re-using layouts and markup across a website.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Pipes&lt;/strong&gt; - I use these for processing and minimising SCSS, and also for resizing and manipulating images.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Configuration&lt;/strong&gt; - Super simple, from a single file. Changes automatically get reflected in the site when hot-reloading.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Speed&lt;/strong&gt; - Dev environment boots in milliseconds, and the whole production site builds in &lt;500ms.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Simplicity&lt;/strong&gt; - No dependencies, other than the Hugo binary itself.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Content-focused&lt;/strong&gt; - Super quick and easy to add new content, rebuild, and deploy.&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;The minimal styles, plus the fact that I use generally-available system fonts and scaled images (thanks to Hugo &lt;a href=&#34;https://gohugo.io/hugo-pipes&#34; target=&#34;_blank&#34; rel=&#34;noopener noreferrer&#34; &gt;pipes&lt;/a&gt;) mean that the total size of the homepage, when downloaded and uncompressed, is less than 20kB.&lt;/p&gt;
&lt;p&gt;This means that the site is now a Green Team member of the &lt;a href=&#34;https://512kb.club&#34; target=&#34;_blank&#34; rel=&#34;noopener noreferrer&#34; &gt;512KB Club&lt;/a&gt;, and most of its pages are also measured to &lt;a href=&#34;https://www.websitecarbon.com/website/wilw-dev&#34; target=&#34;_blank&#34; rel=&#34;noopener noreferrer&#34; &gt;result in the production of extremely low CO2 per visit&lt;/a&gt;. Some included JavaScript on the page re-measures every now and again to keep this up-to-date.&lt;/p&gt;
&lt;p&gt;I look forward to building more with Hugo and the community, and perhaps working on and releasing a theme sometime.&lt;/p&gt;
</content:encoded>
    </item>
    
    <item>
      <title>Making Tax Digital and Plain-Text Accounting</title>
      <link>https://wilw.dev/blog/2022/07/30/mtd-plain-text-accounting/</link>
      <pubDate>Sat, 30 Jul 2022 22:25:00 +0000</pubDate>
      
      <guid>https://wilw.dev/blog/2022/07/30/mtd-plain-text-accounting/</guid>
      
        <category>opinion</category>
      
        <category>technology</category>
      
        <category>finance</category>
      
      
        <enclosure url="https://wilw.dev/media/blog/header-digital-tax.png" type="image/png"/>
      
      <content:encoded>&lt;h2 id=&#34;hmrcs-making-tax-digital&#34;&gt;HMRC’s Making Tax Digital&lt;/h2&gt;
&lt;p&gt;&lt;a href=&#34;https://www.gov.uk/government/publications/making-tax-digital/overview-of-making-tax-digital&#34; target=&#34;_blank&#34; rel=&#34;noopener noreferrer&#34; &gt;Making Tax Digital&lt;/a&gt; (MTD) is part of the UK Government’s plan for modernising the tax system for both businesses and individuals.&lt;/p&gt;
&lt;p&gt;For years, &lt;a href=&#34;https://www.gov.uk/government/organisations/hm-revenue-customs&#34; target=&#34;_blank&#34; rel=&#34;noopener noreferrer&#34; &gt;HMRC&lt;/a&gt; (the Government’s tax department) has had an online tax system that is infamously complicated and slow to use and update such that even accomplishing simple tasks can be long and painful processes. Part of this is due to the laughably complicated UK tax system itself (rather than the fault of the technology), but some of it can certainly be attributed to the antiquated tooling.&lt;/p&gt;
&lt;p&gt;The main idea behind MTD is that tax-payers make use of accounting software - such as &lt;a href=&#34;https://www.xero.com&#34; target=&#34;_blank&#34; rel=&#34;noopener noreferrer&#34; &gt;Xero&lt;/a&gt; or &lt;a href=&#34;https://quickbooks.intuit.com&#34; target=&#34;_blank&#34; rel=&#34;noopener noreferrer&#34; &gt;Quickbooks&lt;/a&gt; - to automate their tax returns. These commercial systems are often more nicely-designed and well-maintained than HMRC’s own, and so it does make sense to use them if you are looking for a more pain-free tax experience.&lt;/p&gt;
&lt;p&gt;The positive effect of using such a tool is also amplified if you already use one of these systems for your bookkeeping anyway. In these cases, the accounting system is naturally aware of all your transactions and tax liabilities, such that submitting returns via MTD can be as simple as a couple of button clicks.&lt;/p&gt;
&lt;p&gt;If you’re not currently using this types of software for bookkeeping (for example, if you’re a smaller business just using some simple spreadsheets), then you’ll have to change your business processes. And since the MTD-compatible tools &lt;a href=&#34;https://www.tax.service.gov.uk/making-tax-digital-software&#34; target=&#34;_blank&#34; rel=&#34;noopener noreferrer&#34; &gt;mostly seem to be commercial SaaS&lt;/a&gt; systems, this will likely come at an additional cost.&lt;/p&gt;
&lt;h2 id=&#34;the-impact-on-plain-text-accounting&#34;&gt;The impact on plain-text accounting&lt;/h2&gt;
&lt;p&gt;Those people that follow this blog will know I’m an advocate for the simplicity, flexibility, and transparency provided by plain-text accounting systems - in particular, those compatible with &lt;a href=&#34;https://www.ledger-cli.org&#34; target=&#34;_blank&#34; rel=&#34;noopener noreferrer&#34; &gt;Ledger&lt;/a&gt; and its peers.&lt;/p&gt;
&lt;p&gt;MTD-compatible software must be approved by HMRC, which results in a walled garden and restricts people’s freedom. I don’t know what the approval process involves, but I would imagine successful systems would include some form of integration secret or private authentication mechanism with HMRC. Such arrangements don’t lend themselves well to distributing FOSS or self-managed tools since they would depend on the end-user managing the secrets themselves.&lt;/p&gt;
&lt;p&gt;As such, I fear that the introduction of MTD will rule-out a swathe of plain-text methods and open-source tools for those doing their accounting and bookkeeping in the UK. Sure, one can use Ledger to periodically export CSV data and import this into an “approved” system for completing a tax return, but do people really have time for this? And why should they continue to use the non-approved methods when they already have to pay for the MTD-compatible ones?&lt;/p&gt;
&lt;p&gt;I am fully on-board with modernising the tax system, but I feel that if this was instead achieved through providing open APIs (authenticated with OAuth2 or similar and without the requirement for app-wide secrets), then it would enable the open-source community to develop systems and tools to compete with the commercial offerings and help to keep things fair for everyone. Governments mandating the use of specific for-profit companies for non-optional activities just doesn’t feel right. Many people in the UK are still wary of this after the Government approach to the COVID pandemic, with dubious contract awards for PPE and testing.&lt;/p&gt;
&lt;p&gt;Making Tax Digital is still relatively young. In fact, as far as I know, it’s only currently mandatory in some sitations relating to VAT for businesses. However, there is an objective to gradually roll it out more comprehensively for business and individual tax over the coming years, and I really hope that the HMRC APIs can be developed in a fair and non-exclusive way.&lt;/p&gt;
</content:encoded>
    </item>
    
    <item>
      <title>Switching-up my workouts 🏋️‍♀️</title>
      <link>https://wilw.dev/blog/2022/07/20/workout-switchup/</link>
      <pubDate>Wed, 20 Jul 2022 19:56:00 +0000</pubDate>
      
      <guid>https://wilw.dev/blog/2022/07/20/workout-switchup/</guid>
      
        <category>life</category>
      
      
        <enclosure url="https://wilw.dev/media/blog/header-gym.png" type="image/png"/>
      
      <content:encoded>&lt;h2 id=&#34;starting-to-workout&#34;&gt;Starting to workout&lt;/h2&gt;
&lt;p&gt;I’ve worked out every day - pretty much - now since March 2020. The only days I’ve missed were because of illness (Covid), or another reason that made it physically (e.g. my dislocated shoulder) or logistically (e.g. during travel) impossible.&lt;/p&gt;
&lt;p&gt;These workouts have pretty much always been “at home”, or wherever I happen to be staying at during the time. They started during the first wave of UK Covid lockdowns, in which I realised I was not getting the exercise I was previously used to when walking the 3 kilometres daily to work and back.&lt;/p&gt;
&lt;p&gt;My brother happened to be living with me at the time, and he had recently got into using the &lt;a href=&#34;https://fitbod.me&#34; target=&#34;_blank&#34; rel=&#34;noopener noreferrer&#34; &gt;Fitbod&lt;/a&gt; app to help guide his exercise. He recommended it to me, and - since I happened to have a pair of dumbells at home - I started giving it a go.&lt;/p&gt;
&lt;p&gt;The app lets you configure your workout goals (e.g. muscle growth, toning, etc.), frequency, and duration. You can also provide it with the types of equipment you have available and any exercises you want to avoid (for example, lunges are hell for my knees, so I avoid these at all costs). Once setup, it creates a workout regime for you, targeting different muscle groups whilst respecting the muscle recovery. Depending on your goal, the app also automatically adjusts the intensity/weight/duration/reps of your exercises as it learns what your current limits are.&lt;/p&gt;
&lt;p&gt;Fitbod keeps things varied, and so I ended up using it daily for over 18 months. I learned a lot about different types of exercises, and how these target the various areas of the body. When I was away from home and the weights, I configured workouts that would rely only on body weight or strap workouts.&lt;/p&gt;
&lt;p&gt;On a personal level, I also noticed hugely positive changes in my body shape. My increased fitness also resulted in better sleep and a more positive daily existence. This all happened more quickly than I had anticipated, too, and I soon got to the point where my body felt like it was almost protesting its own laziness on those rare days I was unable to exercise.&lt;/p&gt;
&lt;h2 id=&#34;change-of-routine&#34;&gt;Change of routine&lt;/h2&gt;
&lt;p&gt;When I dislocated my shoulder back in October 2021, I had to stop working out for a short while. Even though it was only a shoulder injury, my arm was in a sling which made any form of exercise virtually impossible. After a few weeks, I could lose the sling, but I still couldn’t really exercise it whilst things healed.&lt;/p&gt;
&lt;p&gt;Fitbod didn’t have the capacity to understand shoulder injuries, and so ploughed on in reminding me to workout every day and suggesting now-impossible workouts. It was frustrating, so I uninstalled the app with the aim of re-downloading it at a later time.&lt;/p&gt;
&lt;p&gt;I was soon able to introduce small exercises every day. For example, some core exercises and squats. I used my learning from all the types of workouts Fitbod had introduced me to in order to create my own balanced workouts that worked around my needs as my body got better.&lt;/p&gt;
&lt;p&gt;During the weeks and months in which I recovered, I gradually built up my own workouts further, and I realised that I didn’t need Fitbod anymore. I had enough of my own momentum to ensure I kept up the daily routine, and I was able to choose the exercises and routines I enjoyed the most, whilst respecting my muscle recovery.&lt;/p&gt;
&lt;p&gt;I’m not a serious weightlifter or gym-goer, but I’m now at the point where I am confident in selecting the workouts that are right for me, without needing to rely on being told what to do by an app. I take inspiration from friends who suggest new exercises, and I shake things up myself to keep things interesting. I still notice progress and I think I’m at the point now where I mainly just want to maintain my level of fitness and body shape.&lt;/p&gt;
&lt;h2 id=&#34;a-changed-person-&#34;&gt;A changed person? 🤔&lt;/h2&gt;
&lt;p&gt;If you had said - five years ago - that I would now be the type of person that enjoys and actively works out every day, I (along with my friends, family, and others that know me) would have laughed at you. It’s amazing how much I feel I have changed over the last two years.&lt;/p&gt;
&lt;p&gt;This has also come with a greater understanding of my mental health and wellbeing, and how to help manage these aspects of myself. Altogether, I definitely feel that I am much better equipped to develop and adapt as life goes on. ✌️&lt;/p&gt;
</content:encoded>
    </item>
    
    <item>
      <title>My setup for coding on iPad</title>
      <link>https://wilw.dev/blog/2022/06/12/ipad-coding/</link>
      <pubDate>Sun, 12 Jun 2022 11:42:00 +0000</pubDate>
      
      <guid>https://wilw.dev/blog/2022/06/12/ipad-coding/</guid>
      
        <category>technology</category>
      
        <category>selfhost</category>
      
        <category>development</category>
      
      
        <enclosure url="https://wilw.dev/media/blog/header-ipad-coding.png" type="image/png"/>
      
      <content:encoded>&lt;p&gt;Since getting a &lt;a href=&#34;https://www.apple.com/uk/shop/product/MXQT2B/A/magic-keyboard-for-ipad-pro-11-inch-3rd-generation-and-ipad-air-5th-generation-british-english-black&#34; target=&#34;_blank&#34; rel=&#34;noopener noreferrer&#34; &gt;Magic Keyboard&lt;/a&gt; for my iPad Pro, I’ve been using the iPad for many areas of work for which before I would have needed a laptop.&lt;/p&gt;
&lt;p&gt;In fact, last week I was able to use the iPad full-time when at work in our new office space, and I didn’t need to reach for my MacBook once. When I can, I prefer working on the iPad due to its flexibility, brilliant display, speed, battery life, and more.&lt;/p&gt;
&lt;p&gt;I also prefer working in the single-context view iPadOS provides. On a Mac I would have dozens of windows open at any one time, offering lots of distraction. On iPad, I generally use one screen at a time, allowing me to keep focus. In in-person meetings, the iPad’s form-factor also feels less of a psychological barrier between myself and my colleagues. In video meetings, the iPad also holds up well, and its screen-sharing functionality is fantastic.&lt;/p&gt;
&lt;p&gt;My role has changed in recent years and, as a result, I do less coding in my day-to-day work at my job. However, this is one area where native iPad working does fall down, since the system does not offer native command line tooling or the ability to install and run development software. During the past week there were a couple of times where it would’ve been useful for me to contribute some code with my team and - not wanting to have to return to the MacBook just for these moments - I spent a little time setting-up a development environment on my iPad.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;In this post I will talk a little about my approach to setting-up a development workflow from my iPad, using a VPS, Tailscale, Termius, and Code Server.&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;✌️ As a bonus, all of the steps in this post can also be completed on an iPad. This post was also written, and the site was built and deployed, from my iPad.&lt;/p&gt;
&lt;h2 id=&#34;1-provision-a-vps&#34;&gt;1. Provision a VPS&lt;/h2&gt;
&lt;p&gt;Since the iPad itself does not have the tooling needed to write, run, and build applications, I provisioned a Linux cloud server that would do most of the heavy lifting.&lt;/p&gt;
&lt;p&gt;I spun up a new Ubuntu 22.04 server on &lt;a href=&#34;https://www.linode.com&#34; target=&#34;_blank&#34; rel=&#34;noopener noreferrer&#34; &gt;Linode&lt;/a&gt; with sufficient resources needed to run and build my apps. I went for 4GB memory and 2 vCPUs, but Linode lets you change this later if you realise you need less or more resources. My intention is to use this machine solely for development work.&lt;/p&gt;
&lt;h2 id=&#34;2-configure-docker-and-initial-server-set-up&#34;&gt;2. Configure Docker and initial server set-up&lt;/h2&gt;
&lt;p&gt;The next steps were to access the new server and set-up the environment.&lt;/p&gt;
&lt;p&gt;&lt;a href=&#34;https://termius.com&#34; target=&#34;_blank&#34; rel=&#34;noopener noreferrer&#34; &gt;Termius&lt;/a&gt; is an excellent app for managing remote SSH and file-transfer sessions, and it’s even better on iPad with Magic Keyboard. It’s free to use but their premium subscription is worth it too.&lt;/p&gt;
&lt;p&gt;Using Termius, I connected to the new server (using my SSH key) and firstly installed Docker and Docker Compose (which I will use to run the Code Server instance):&lt;/p&gt;
&lt;div class=&#34;highlight&#34;&gt;&lt;pre tabindex=&#34;0&#34; style=&#34;background-color:#f0f3f3;-moz-tab-size:2;-o-tab-size:2;tab-size:2;&#34;&gt;&lt;code class=&#34;language-shell&#34; data-lang=&#34;shell&#34;&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;&lt;span style=&#34;color:#09f;font-style:italic&#34;&gt;# apt update&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;&lt;span style=&#34;color:#09f;font-style:italic&#34;&gt;# apt install docker docker-compose&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;&lt;span style=&#34;color:#09f;font-style:italic&#34;&gt;# service docker start&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;Next, I created a user that I would use for management and for running the Docker containers:&lt;/p&gt;
&lt;div class=&#34;highlight&#34;&gt;&lt;pre tabindex=&#34;0&#34; style=&#34;background-color:#f0f3f3;-moz-tab-size:2;-o-tab-size:2;tab-size:2;&#34;&gt;&lt;code class=&#34;language-shell&#34; data-lang=&#34;shell&#34;&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;&lt;span style=&#34;color:#09f;font-style:italic&#34;&gt;# useradd will # ... go through setup&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;&lt;span style=&#34;color:#09f;font-style:italic&#34;&gt;# usermod -aG docker will&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;I recommend editing &lt;code&gt;/etc/ssh/sshd_config&lt;/code&gt; to harden your SSH setup, restrict root login and only permit key-based authentication. Don’t forget to restart the SSHD service after changing the settings. You should also add any needed SSH keys to your non-root user’s home directory so that they can login later.&lt;/p&gt;
&lt;h2 id=&#34;3-configure-tailscale-and-firewalls&#34;&gt;3. Configure Tailscale and firewalls&lt;/h2&gt;
&lt;p&gt;&lt;a href=&#34;https://www.tailscale.com&#34; target=&#34;_blank&#34; rel=&#34;noopener noreferrer&#34; &gt;Tailscale&lt;/a&gt; is a fantastic service that allows you to setup a VPN mesh network for all of your devices, based on the &lt;a href=&#34;https://www.wireguard.com&#34; target=&#34;_blank&#34; rel=&#34;noopener noreferrer&#34; &gt;Wireguard&lt;/a&gt; protocol. By leveraging Tailscale, we can ensure that access to the server is restricted only to devices you’ve added to your Tailnet.&lt;/p&gt;
&lt;p&gt;First, create an account with Tailscale if you haven’t got one already (it’s free for up to 20 devices!) and then &lt;a href=&#34;https://tailscale.com/download/linux&#34; target=&#34;_blank&#34; rel=&#34;noopener noreferrer&#34; &gt;install Tailscale&lt;/a&gt; on the server:&lt;/p&gt;
&lt;div class=&#34;highlight&#34;&gt;&lt;pre tabindex=&#34;0&#34; style=&#34;background-color:#f0f3f3;-moz-tab-size:2;-o-tab-size:2;tab-size:2;&#34;&gt;&lt;code class=&#34;language-shell&#34; data-lang=&#34;shell&#34;&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;&lt;span style=&#34;color:#09f;font-style:italic&#34;&gt;# curl -fsSL https://tailscale.com/install.sh | sh&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;&lt;span style=&#34;color:#09f;font-style:italic&#34;&gt;# tailscale up&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;Follow the instructions to authorise the machine with your Tailscale account, and then you’ll be able to see the server listed on your Tailscale’s account webpage.&lt;/p&gt;
&lt;p&gt;&lt;img src=&#34;https://wilw.dev/media/blog/ipad-coding1.jpg&#34; alt=&#34;Tailscale website showing my development server listed amongst my devices&#34;&gt;&lt;/p&gt;
&lt;p&gt;Next, ensure you have the &lt;a href=&#34;https://apps.apple.com/gb/app/tailscale/id1470499037&#34; target=&#34;_blank&#34; rel=&#34;noopener noreferrer&#34; &gt;Tailscale app&lt;/a&gt; installed and connected for your iPad. Your iPad should then also become listed on the Tailscale website.&lt;/p&gt;
&lt;p&gt;Finally, configure your firewalls to block all public access. If you use Linode, you can make use of their cloud firewall service to drop all inbound traffic.&lt;/p&gt;
&lt;p&gt;&lt;img src=&#34;https://wilw.dev/media/blog/ipad-coding2.PNG&#34; alt=&#34;Linode dashboard showing a firewall that is dropping all traffic&#34;&gt;&lt;/p&gt;
&lt;p&gt;Finally, verify that you can still connect to the development server by modifying your VPS IP address in Termius to the Tailscale one shown on the website (or the DNS name if you’ve enabled MagicDNS!).&lt;/p&gt;
&lt;p&gt;You can now be sure that only your authorised Tailscale devices can talk to your server.&lt;/p&gt;
&lt;h2 id=&#34;4-run-code-server&#34;&gt;4. Run Code Server&lt;/h2&gt;
&lt;p&gt;Use Termius to log back into your development VPS with your non-root user. We’ll now set-up &lt;a href=&#34;https://github.com/coder/code-server&#34; target=&#34;_blank&#34; rel=&#34;noopener noreferrer&#34; &gt;Code Server&lt;/a&gt; on the server.&lt;/p&gt;
&lt;p&gt;Code Server is a web-based implementation of Microsoft’s VSCode. &lt;a href=&#34;https://www.linuxserver.io&#34; target=&#34;_blank&#34; rel=&#34;noopener noreferrer&#34; &gt;linuxserver.io&lt;/a&gt; maintain a community Docker image, which is what I use.&lt;/p&gt;
&lt;p&gt;Create a &lt;code&gt;docker-compose.yml&lt;/code&gt; file on your server with these contents:&lt;/p&gt;
&lt;div class=&#34;highlight&#34;&gt;&lt;pre tabindex=&#34;0&#34; style=&#34;background-color:#f0f3f3;-moz-tab-size:2;-o-tab-size:2;tab-size:2;&#34;&gt;&lt;code class=&#34;language-yaml&#34; data-lang=&#34;yaml&#34;&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;&lt;span style=&#34;color:#309;font-weight:bold&#34;&gt;version&lt;/span&gt;:&lt;span style=&#34;color:#bbb&#34;&gt; &lt;/span&gt;&lt;span style=&#34;color:#c30&#34;&gt;&#34;2.1&#34;&lt;/span&gt;&lt;span style=&#34;color:#bbb&#34;&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;&lt;span style=&#34;color:#bbb&#34;&gt;&lt;/span&gt;&lt;span style=&#34;color:#309;font-weight:bold&#34;&gt;services&lt;/span&gt;:&lt;span style=&#34;color:#bbb&#34;&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;&lt;span style=&#34;color:#bbb&#34;&gt;  &lt;/span&gt;&lt;span style=&#34;color:#309;font-weight:bold&#34;&gt;code-server&lt;/span&gt;:&lt;span style=&#34;color:#bbb&#34;&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;&lt;span style=&#34;color:#bbb&#34;&gt;    &lt;/span&gt;&lt;span style=&#34;color:#309;font-weight:bold&#34;&gt;image&lt;/span&gt;:&lt;span style=&#34;color:#bbb&#34;&gt; &lt;/span&gt;lscr.io/linuxserver/code-server:latest&lt;span style=&#34;color:#bbb&#34;&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;&lt;span style=&#34;color:#bbb&#34;&gt;    &lt;/span&gt;&lt;span style=&#34;color:#309;font-weight:bold&#34;&gt;container_name&lt;/span&gt;:&lt;span style=&#34;color:#bbb&#34;&gt; &lt;/span&gt;code-server&lt;span style=&#34;color:#bbb&#34;&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;&lt;span style=&#34;color:#bbb&#34;&gt;    &lt;/span&gt;&lt;span style=&#34;color:#309;font-weight:bold&#34;&gt;environment&lt;/span&gt;:&lt;span style=&#34;color:#bbb&#34;&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;&lt;span style=&#34;color:#bbb&#34;&gt;      &lt;/span&gt;- PUID=1000&lt;span style=&#34;color:#bbb&#34;&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;&lt;span style=&#34;color:#bbb&#34;&gt;      &lt;/span&gt;- PGID=1000&lt;span style=&#34;color:#bbb&#34;&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;&lt;span style=&#34;color:#bbb&#34;&gt;      &lt;/span&gt;- TZ=Europe/London&lt;span style=&#34;color:#bbb&#34;&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;&lt;span style=&#34;color:#bbb&#34;&gt;      &lt;/span&gt;- PASSWORD=password&lt;span style=&#34;color:#bbb&#34;&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;&lt;span style=&#34;color:#bbb&#34;&gt;      &lt;/span&gt;- SUDO_PASSWORD=password&lt;span style=&#34;color:#bbb&#34;&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;&lt;span style=&#34;color:#bbb&#34;&gt;      &lt;/span&gt;- PROXY_DOMAIN=development&lt;span style=&#34;color:#bbb&#34;&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;&lt;span style=&#34;color:#bbb&#34;&gt;      &lt;/span&gt;- DEFAULT_WORKSPACE=/config/workspace&lt;span style=&#34;color:#bbb&#34;&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;&lt;span style=&#34;color:#bbb&#34;&gt;    &lt;/span&gt;&lt;span style=&#34;color:#309;font-weight:bold&#34;&gt;volumes&lt;/span&gt;:&lt;span style=&#34;color:#bbb&#34;&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;&lt;span style=&#34;color:#bbb&#34;&gt;      &lt;/span&gt;- ./code:/config&lt;span style=&#34;color:#bbb&#34;&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;&lt;span style=&#34;color:#bbb&#34;&gt;    &lt;/span&gt;&lt;span style=&#34;color:#309;font-weight:bold&#34;&gt;ports&lt;/span&gt;:&lt;span style=&#34;color:#bbb&#34;&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;&lt;span style=&#34;color:#bbb&#34;&gt;      &lt;/span&gt;- &lt;span style=&#34;color:#f60&#34;&gt;8443&lt;/span&gt;:&lt;span style=&#34;color:#f60&#34;&gt;8443&lt;/span&gt;&lt;span style=&#34;color:#bbb&#34;&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;&lt;span style=&#34;color:#bbb&#34;&gt;    &lt;/span&gt;&lt;span style=&#34;color:#309;font-weight:bold&#34;&gt;restart&lt;/span&gt;:&lt;span style=&#34;color:#bbb&#34;&gt; &lt;/span&gt;unless-stopped&lt;span style=&#34;color:#bbb&#34;&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;Change the values for the password (used to access the Code web interface) and the &lt;code&gt;sudo&lt;/code&gt; password (which you can use to add extra packages to the container later).&lt;/p&gt;
&lt;p&gt;When happy, bring up the Code Server with &lt;code&gt;docker-compose up -d&lt;/code&gt;. After a few seconds, you should be able to visit your Code Server instance in Safari by navigating to the Tailscale IP address (or MagicDNS entry) on port &lt;code&gt;8443&lt;/code&gt;.&lt;/p&gt;
&lt;p&gt;You can now use Code in much the same way as you would with the desktop app.&lt;/p&gt;
&lt;p&gt;&lt;img src=&#34;https://wilw.dev/media/blog/ipad-coding3.PNG&#34; alt=&#34;My Code Server running in Safari&#34;&gt;&lt;/p&gt;
&lt;h2 id=&#34;5-post-setup-tasks&#34;&gt;5. Post-setup tasks&lt;/h2&gt;
&lt;p&gt;Whilst you can now write code via Safari on your iPad, extra setup will be needed to actually do anything with the code.&lt;/p&gt;
&lt;p&gt;When managing the Code Server container, I recommend simply opening a Terminal window from within the Code Server web interface itself. You can use the &lt;code&gt;SUDO_PASSWORD&lt;/code&gt; you configured earlier for tasks that require root access.&lt;/p&gt;
&lt;p&gt;For example, to set up remote git properly, you’ll want to create a new SSH keypair and add the public key to your git host.&lt;/p&gt;
&lt;p&gt;If you need extra software - for example, Node/yarn for JS apps, Python, Ruby, or anything else - then install these via &lt;code&gt;apt&lt;/code&gt;. Such changes should persist properly between container restarts, meaning that you can build out your development environment in the way that suits you best.&lt;/p&gt;
&lt;p&gt;If you want to be able to run your apps - for example a Python Flask app on port 5000 - and access via a browser, then just add a mapping for the port to your &lt;code&gt;docker-compose.yml&lt;/code&gt; file.&lt;/p&gt;
&lt;h2 id=&#34;optional-next-steps&#34;&gt;Optional next steps&lt;/h2&gt;
&lt;p&gt;Although Tailscale is encrypting your traffic, you may want to provision a TLS certficate and use a reverse-proxy, like &lt;a href=&#34;https://doc.traefik.io/traefik&#34; target=&#34;_blank&#34; rel=&#34;noopener noreferrer&#34; &gt;Traefik&lt;/a&gt;, to serve Code Server.&lt;/p&gt;
&lt;p&gt;To do so, I recommend following &lt;a href=&#34;https://tailscale.com/kb/1153/enabling-https/&#34; target=&#34;_blank&#34; rel=&#34;noopener noreferrer&#34; &gt;the instructions&lt;/a&gt; on the Tailscale website.&lt;/p&gt;
&lt;h2 id=&#34;conclusion&#34;&gt;Conclusion&lt;/h2&gt;
&lt;p&gt;Whilst the approach in this post will provide a working development environment for writing, building, and deploying many types of software, it isn’t suitable for all apps.&lt;/p&gt;
&lt;p&gt;For example, mobile development (in which a connected device or simulator might be required) will be tricky. Similarly, coding for Apple devices (macOS or iOS) requires XCode, which is currently only available on MacOS.&lt;/p&gt;
&lt;p&gt;Even web development can be a little fiddly without direct access to developer tools in a browser. However, for light usage, I really enjoy this approach and I am excited about the possibilities as the ecosystem continues to develop.&lt;/p&gt;
</content:encoded>
    </item>
    
    <item>
      <title>Parcel to the rescue</title>
      <link>https://wilw.dev/blog/2022/05/25/parcel/</link>
      <pubDate>Wed, 25 May 2022 17:59:00 +0000</pubDate>
      
      <guid>https://wilw.dev/blog/2022/05/25/parcel/</guid>
      
        <category>technology</category>
      
        <category>javascript</category>
      
        <category>development</category>
      
      
      <content:encoded>&lt;p&gt;Earlier this week I needed to make some changes and re-deploy an old &lt;a href=&#34;https://vuejs.org&#34; target=&#34;_blank&#34; rel=&#34;noopener noreferrer&#34; &gt;Vue&lt;/a&gt; app. I hadn’t touched the codebase in over a year, and my experience with the rate of change in the front-end web space made me dread what would happen if I tried to re-awaken this thing.&lt;/p&gt;
&lt;p&gt;Sure enough, after running a &lt;code&gt;yarn install&lt;/code&gt; and launching the app using the scripts in &lt;code&gt;package.json&lt;/code&gt;, a number of errors were displayed about Node/Webpack/Vue incompatibilities, and I didn’t really know where to start. I don’t use Vue on a daily basis these days, and so I don’t usually need to make an effort to keep fully up-to-date on its developments, but I knew I was several versions behind on &lt;code&gt;vue&lt;/code&gt;, &lt;code&gt;vue-loader&lt;/code&gt;, as well as all the &lt;code&gt;sass&lt;/code&gt; and &lt;code&gt;babel&lt;/code&gt; toolings. This wasn’t going to be a quick fix.&lt;/p&gt;
&lt;p&gt;From the errors displayed on the console, I could see that the problem was largely related to the relationships between the various build tools. When I worked on this project more full-time (or even still today?) the &lt;a href=&#34;https://cli.vuejs.org&#34; target=&#34;_blank&#34; rel=&#34;noopener noreferrer&#34; &gt;&lt;code&gt;vue-cli&lt;/code&gt;&lt;/a&gt; would manage the build process for you, and would create a number of files and directories containing various loader configuration setups and Webpack build files.&lt;/p&gt;
&lt;p&gt;The problem was probably to do with &lt;em&gt;something&lt;/em&gt; in those files no longer working - which is frustrating given the version-locked dependencies that are supposed to stop this kind of thing from happening. I didn’t have the time, expertise, or energy to try and debug this complex setup, and I couldn’t find many resources that helped me in migrating the build tools.&lt;/p&gt;
&lt;h2 id=&#34;could-parcel-help&#34;&gt;Could Parcel help?&lt;/h2&gt;
&lt;p&gt;In another project, I had recently used &lt;a href=&#34;https://parceljs.org&#34; target=&#34;_blank&#34; rel=&#34;noopener noreferrer&#34; &gt;Parcel&lt;/a&gt; to manage webapp build processes. The tool promises to be “zero config” out of the box and - in my experience - it had been.&lt;/p&gt;
&lt;p&gt;I saw that Parcel &lt;a href=&#34;https://parceljs.org/languages/vue&#34; target=&#34;_blank&#34; rel=&#34;noopener noreferrer&#34; &gt;supports Vue&lt;/a&gt; and thought I’d see if it could help me solve my problems. I made a few tweaks in the codebase structure and small changes to make the app Vue 3 compatible (this was relatively straight forward) and I bumped a couple of other packages (such as the router).&lt;/p&gt;
&lt;p&gt;I added the Parcel dependency and also removed a number of other dependencies, such as Webpack and &lt;code&gt;babel&lt;/code&gt;-related ones, along with the various &lt;code&gt;-loader&lt;/code&gt;s needed by Webpack. I launched the app using &lt;code&gt;parcel&lt;/code&gt; and… it built! I got a hot-reloading development environment without any fuss. Awesome.&lt;/p&gt;
&lt;p&gt;I was able to make the changes I needed to the app, build, and deploy it as a static webapp - all using Parcel and now with a cleaner and leaner project structure and dependency list.&lt;/p&gt;
&lt;h2 id=&#34;alternatives&#34;&gt;Alternatives&lt;/h2&gt;
&lt;p&gt;I understand that, these days, Webpack 5 is supposed to also be “config-less” for most setups, but their docs explain that &lt;a href=&#34;https://webpack.js.org/concepts/#loaders&#34; target=&#34;_blank&#34; rel=&#34;noopener noreferrer&#34; &gt;this is only the case for &lt;code&gt;js&lt;/code&gt; and &lt;code&gt;json&lt;/code&gt; files&lt;/a&gt;. If you want anything else, such as images or stylesheets, you’ll need extra loaders in your dependencies and extra configuration files to write and maintain.&lt;/p&gt;
&lt;p&gt;More recently, I’ve heard great things about &lt;a href=&#34;https://vitejs.dev&#34; target=&#34;_blank&#34; rel=&#34;noopener noreferrer&#34; &gt;Vite&lt;/a&gt; too, but I’ve not yet had a chance to test this out.&lt;/p&gt;
&lt;p&gt;Either way, if you want to build and manage your web projects without a fuss, then I can certainly recommend taking &lt;a href=&#34;https://parceljs.org&#34; target=&#34;_blank&#34; rel=&#34;noopener noreferrer&#34; &gt;Parcel&lt;/a&gt; for a spin.&lt;/p&gt;
</content:encoded>
    </item>
    
    <item>
      <title>Simple Ledger visualisations using Python</title>
      <link>https://wilw.dev/blog/2022/04/24/ledger-python-visualisation/</link>
      <pubDate>Sun, 24 Apr 2022 14:39:00 +0000</pubDate>
      
      <guid>https://wilw.dev/blog/2022/04/24/ledger-python-visualisation/</guid>
      
        <category>technology</category>
      
        <category>python</category>
      
        <category>ledger</category>
      
        <category>finance</category>
      
      
      <content:encoded>&lt;p&gt;If you’re a current follower of this blog then you may already know that I’m a bit of a fan of using plain text accounting for managing finances.&lt;/p&gt;
&lt;p&gt;I mainly use the &lt;a href=&#34;https://www.ledger-cli.org&#34; target=&#34;_blank&#34; rel=&#34;noopener noreferrer&#34; &gt;Ledger&lt;/a&gt; text file format and CLI tool for bookkeeping and reporting on finances. This works great, and I can quickly and easily generate different kinds of reports using a range of simple Ledger commands.&lt;/p&gt;
&lt;p&gt;For example, to generate a quick income/expenses balance sheet for a particular date range I can run &lt;code&gt;ledger balance income expense -b 2022/03/01 -e 2022/03/31&lt;/code&gt;, which produces something along the lines of the following:&lt;/p&gt;
&lt;pre tabindex=&#34;0&#34;&gt;&lt;code&gt;             £381.50  Expenses
             £129.87    Hosting
             £185.00    Services:Accountancy
              £66.63    Software
          £-1,000.00  Income:Sales:Contracting
--------------------
            £-618.50
&lt;/code&gt;&lt;/pre&gt;&lt;p&gt;Whilst this approach allows for easy numeric reporting for accounting purposes, by default there are no built-in ways for &lt;em&gt;visualising&lt;/em&gt; the flow of money.&lt;/p&gt;
&lt;p&gt;In this post I’ll talk a little about how to use Python to generate a simple visualisation of financial flow when provided with a Ledger export. This consists of three key steps:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;Outputting data from Ledger in a readable format;&lt;/li&gt;
&lt;li&gt;Processing the data;&lt;/li&gt;
&lt;li&gt;Outputting a diagram that visually represents the data.&lt;/li&gt;
&lt;/ul&gt;
&lt;h2 id=&#34;step-1-outputting-ledger-data&#34;&gt;Step 1: Outputting Ledger data&lt;/h2&gt;
&lt;p&gt;Ledger provides a number of commands that allow for exporting and reporting on a ledger file. I recommend reading &lt;a href=&#34;https://www.ledger-cli.org/3.0/doc/ledger3.html&#34; target=&#34;_blank&#34; rel=&#34;noopener noreferrer&#34; &gt;the documentation&lt;/a&gt; for a full idea. However, in this case, we can simlpy use the &lt;code&gt;csv&lt;/code&gt; command to generate an output that can be easily piped into a Python program.&lt;/p&gt;
&lt;p&gt;For example, &lt;code&gt;ledger csv income expense -b 2022/03/01 -e 2022/03/31&lt;/code&gt; produces an output with each transaction on its own row in CSV format, along the lines of below:&lt;/p&gt;
&lt;pre tabindex=&#34;0&#34;&gt;&lt;code class=&#34;language-csv&#34; data-lang=&#34;csv&#34;&gt;&#34;2022/03/01&#34;,&#34;&#34;,&#34;Linode&#34;,&#34;Expenses:Hosting&#34;,&#34;£&#34;,&#34;40.58&#34;,&#34;&#34;,&#34;&#34;
&#34;2022/03/01&#34;,&#34;&#34;,&#34;Company1&#34;,&#34;Income:Consultancy&#34;,&#34;£&#34;,&#34;-120.5&#34;,&#34;&#34;,&#34;&#34;
&#34;2022/03/01&#34;,&#34;&#34;,&#34;Accountancy Company&#34;,&#34;Expenses:Services&#34;,&#34;£&#34;,&#34;200.28&#34;,&#34;&#34;,&#34;&#34;
&#34;2022/03/03&#34;,&#34;&#34;,&#34;Company2&#34;,&#34;Income:Sales&#34;,&#34;£&#34;,&#34;-900.0&#34;,&#34;&#34;,&#34;&#34;
...
&lt;/code&gt;&lt;/pre&gt;&lt;p&gt;We can write some simple Python now to receive this data into our program. Create a new Python source file (e.g. &lt;code&gt;viz.py&lt;/code&gt;) and add these contents:&lt;/p&gt;
&lt;div class=&#34;highlight&#34;&gt;&lt;pre tabindex=&#34;0&#34; style=&#34;background-color:#f0f3f3;-moz-tab-size:2;-o-tab-size:2;tab-size:2;&#34;&gt;&lt;code class=&#34;language-python&#34; data-lang=&#34;python&#34;&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;&lt;span style=&#34;color:#069;font-weight:bold&#34;&gt;import&lt;/span&gt; &lt;span style=&#34;color:#0cf;font-weight:bold&#34;&gt;sys&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;&lt;span style=&#34;color:#09f;font-style:italic&#34;&gt;# Define a Transaction class&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;&lt;span style=&#34;color:#069;font-weight:bold&#34;&gt;class&lt;/span&gt; &lt;span style=&#34;color:#0a8;font-weight:bold&#34;&gt;Transaction&lt;/span&gt;:
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;  &lt;span style=&#34;color:#069;font-weight:bold&#34;&gt;def&lt;/span&gt; __init__(self, account, value):
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;    self&lt;span style=&#34;color:#555&#34;&gt;.&lt;/span&gt;account &lt;span style=&#34;color:#555&#34;&gt;=&lt;/span&gt; account &lt;span style=&#34;color:#09f;font-style:italic&#34;&gt;# The account name for this transaction&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;    self&lt;span style=&#34;color:#555&#34;&gt;.&lt;/span&gt;value &lt;span style=&#34;color:#555&#34;&gt;=&lt;/span&gt; &lt;span style=&#34;color:#f60&#34;&gt;0&lt;/span&gt; &lt;span style=&#34;color:#555&#34;&gt;-&lt;/span&gt; &lt;span style=&#34;color:#366&#34;&gt;float&lt;/span&gt;(value) &lt;span style=&#34;color:#09f;font-style:italic&#34;&gt;# Income is negative&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;&lt;span style=&#34;color:#09f;font-style:italic&#34;&gt;# Prepare a list of transactions by reading from stdin&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;transactions &lt;span style=&#34;color:#555&#34;&gt;=&lt;/span&gt; []
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;&lt;span style=&#34;color:#069;font-weight:bold&#34;&gt;for&lt;/span&gt; line &lt;span style=&#34;color:#000;font-weight:bold&#34;&gt;in&lt;/span&gt; sys&lt;span style=&#34;color:#555&#34;&gt;.&lt;/span&gt;stdin:
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;  line &lt;span style=&#34;color:#555&#34;&gt;=&lt;/span&gt; line&lt;span style=&#34;color:#555&#34;&gt;.&lt;/span&gt;replace(&lt;span style=&#34;color:#c30&#34;&gt;&#39;&#34;&#39;&lt;/span&gt;, &lt;span style=&#34;color:#c30&#34;&gt;&#39;&#39;&lt;/span&gt;)&lt;span style=&#34;color:#555&#34;&gt;.&lt;/span&gt;split(&lt;span style=&#34;color:#c30&#34;&gt;&#39;,&#39;&lt;/span&gt;)
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;  transactions&lt;span style=&#34;color:#555&#34;&gt;.&lt;/span&gt;append(
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;    Transaction(line[&lt;span style=&#34;color:#f60&#34;&gt;3&lt;/span&gt;], line[&lt;span style=&#34;color:#f60&#34;&gt;6&lt;/span&gt;])
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;  )
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;The code above reads in lines from &lt;code&gt;stdin&lt;/code&gt; and instantiates &lt;code&gt;Transaction&lt;/code&gt; objects for each line. We can invoke this code by piping the output from Ledger directly into the program:&lt;/p&gt;
&lt;pre tabindex=&#34;0&#34;&gt;&lt;code&gt;ledger csv income expense -b 2022/03/01 -e 2022/03/31 | python viz.py
&lt;/code&gt;&lt;/pre&gt;&lt;p&gt;In this post, I’m only really interested in the financial flow (i.e. the values of transactions), and so we don’t need to capture payees or other information from the transactions.&lt;/p&gt;
&lt;h2 id=&#34;step-2-data-processing&#34;&gt;Step 2: Data processing&lt;/h2&gt;
&lt;p&gt;The next step is to process the transaction data. My aim is to achieve a &lt;a href=&#34;https://en.wikipedia.org/wiki/Sankey_diagram&#34; target=&#34;_blank&#34; rel=&#34;noopener noreferrer&#34; &gt;Sankey&lt;/a&gt; diagram that shows the flow from income to expense accounts, and to illustrate any profit.&lt;/p&gt;
&lt;p&gt;As such, we need to build some account data for the various accounts in our Ledger file.&lt;/p&gt;
&lt;p&gt;To do so, add the following code to your &lt;code&gt;viz.py&lt;/code&gt; file:&lt;/p&gt;
&lt;div class=&#34;highlight&#34;&gt;&lt;pre tabindex=&#34;0&#34; style=&#34;background-color:#f0f3f3;-moz-tab-size:2;-o-tab-size:2;tab-size:2;&#34;&gt;&lt;code class=&#34;language-python&#34; data-lang=&#34;python&#34;&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;&lt;span style=&#34;color:#09f;font-style:italic&#34;&gt;# Instantiate an Account class&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;&lt;span style=&#34;color:#069;font-weight:bold&#34;&gt;class&lt;/span&gt; &lt;span style=&#34;color:#0a8;font-weight:bold&#34;&gt;Account&lt;/span&gt;:
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;  &lt;span style=&#34;color:#069;font-weight:bold&#34;&gt;def&lt;/span&gt; __init__(self, name, value):
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;    &lt;span style=&#34;color:#366&#34;&gt;print&lt;/span&gt;(name, value)
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;    self&lt;span style=&#34;color:#555&#34;&gt;.&lt;/span&gt;name &lt;span style=&#34;color:#555&#34;&gt;=&lt;/span&gt; name
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;    self&lt;span style=&#34;color:#555&#34;&gt;.&lt;/span&gt;value &lt;span style=&#34;color:#555&#34;&gt;=&lt;/span&gt; value
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;  &lt;span style=&#34;color:#09f;font-style:italic&#34;&gt;# In the Sankey diagram, show income coming in from above and expense going downwards:&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;  &lt;span style=&#34;color:#99f&#34;&gt;@property&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;  &lt;span style=&#34;color:#069;font-weight:bold&#34;&gt;def&lt;/span&gt; &lt;span style=&#34;color:#c0f&#34;&gt;orientation&lt;/span&gt;(self):
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;    &lt;span style=&#34;color:#069;font-weight:bold&#34;&gt;if&lt;/span&gt; self&lt;span style=&#34;color:#555&#34;&gt;.&lt;/span&gt;value &lt;span style=&#34;color:#555&#34;&gt;&lt;&lt;/span&gt; &lt;span style=&#34;color:#f60&#34;&gt;0&lt;/span&gt;: &lt;span style=&#34;color:#069;font-weight:bold&#34;&gt;return&lt;/span&gt; &lt;span style=&#34;color:#555&#34;&gt;-&lt;/span&gt;&lt;span style=&#34;color:#f60&#34;&gt;1&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;    &lt;span style=&#34;color:#069;font-weight:bold&#34;&gt;if&lt;/span&gt; self&lt;span style=&#34;color:#555&#34;&gt;.&lt;/span&gt;value &lt;span style=&#34;color:#555&#34;&gt;&gt;&lt;/span&gt; &lt;span style=&#34;color:#f60&#34;&gt;0&lt;/span&gt;: &lt;span style=&#34;color:#069;font-weight:bold&#34;&gt;return&lt;/span&gt; &lt;span style=&#34;color:#f60&#34;&gt;1&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;accounts &lt;span style=&#34;color:#555&#34;&gt;=&lt;/span&gt; []
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;&lt;span style=&#34;color:#069;font-weight:bold&#34;&gt;for&lt;/span&gt; transaction &lt;span style=&#34;color:#000;font-weight:bold&#34;&gt;in&lt;/span&gt; transactions:
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;  t_accounts &lt;span style=&#34;color:#555&#34;&gt;=&lt;/span&gt; transaction&lt;span style=&#34;color:#555&#34;&gt;.&lt;/span&gt;account&lt;span style=&#34;color:#555&#34;&gt;.&lt;/span&gt;split(&lt;span style=&#34;color:#c30&#34;&gt;&#39;:&#39;&lt;/span&gt;)
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;  top_level &lt;span style=&#34;color:#555&#34;&gt;=&lt;/span&gt; t_accounts[&lt;span style=&#34;color:#f60&#34;&gt;0&lt;/span&gt;]
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;  account &lt;span style=&#34;color:#555&#34;&gt;=&lt;/span&gt; &lt;span style=&#34;color:#366&#34;&gt;next&lt;/span&gt;(&lt;span style=&#34;color:#366&#34;&gt;filter&lt;/span&gt;(&lt;span style=&#34;color:#069;font-weight:bold&#34;&gt;lambda&lt;/span&gt; a: a&lt;span style=&#34;color:#555&#34;&gt;.&lt;/span&gt;name &lt;span style=&#34;color:#555&#34;&gt;==&lt;/span&gt; top_level, accounts), &lt;span style=&#34;color:#069;font-weight:bold&#34;&gt;None&lt;/span&gt;)
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;  &lt;span style=&#34;color:#069;font-weight:bold&#34;&gt;if&lt;/span&gt; &lt;span style=&#34;color:#000;font-weight:bold&#34;&gt;not&lt;/span&gt; account:
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;    account &lt;span style=&#34;color:#555&#34;&gt;=&lt;/span&gt; Account(top_level, &lt;span style=&#34;color:#f60&#34;&gt;0&lt;/span&gt;)
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;    accounts&lt;span style=&#34;color:#555&#34;&gt;.&lt;/span&gt;append(account)
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;  account&lt;span style=&#34;color:#555&#34;&gt;.&lt;/span&gt;value &lt;span style=&#34;color:#555&#34;&gt;&#43;=&lt;/span&gt; transaction&lt;span style=&#34;color:#555&#34;&gt;.&lt;/span&gt;value
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;&lt;span style=&#34;color:#09f;font-style:italic&#34;&gt;# Calculate profit&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;profit &lt;span style=&#34;color:#555&#34;&gt;=&lt;/span&gt; &lt;span style=&#34;color:#366&#34;&gt;sum&lt;/span&gt;(&lt;span style=&#34;color:#366&#34;&gt;map&lt;/span&gt;(&lt;span style=&#34;color:#069;font-weight:bold&#34;&gt;lambda&lt;/span&gt; t: t&lt;span style=&#34;color:#555&#34;&gt;.&lt;/span&gt;value, transactions))
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;&lt;span style=&#34;color:#069;font-weight:bold&#34;&gt;if&lt;/span&gt; profit &lt;span style=&#34;color:#555&#34;&gt;&gt;&lt;/span&gt; &lt;span style=&#34;color:#f60&#34;&gt;0&lt;/span&gt;:
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;  accounts&lt;span style=&#34;color:#555&#34;&gt;.&lt;/span&gt;append(Account(&lt;span style=&#34;color:#c30&#34;&gt;&#39;Profit&#39;&lt;/span&gt;, &lt;span style=&#34;color:#f60&#34;&gt;0&lt;/span&gt; &lt;span style=&#34;color:#555&#34;&gt;-&lt;/span&gt; profit))
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;In the above code, we declare an &lt;code&gt;Account&lt;/code&gt; class and then iterate through our transactions to build up a list of &lt;code&gt;Account&lt;/code&gt; objects, with each having a total value representing the sum of all transactions involving that account. In this code, for now, we only consider the top level accounts (i.e. &lt;code&gt;Income&lt;/code&gt;, &lt;code&gt;Expense&lt;/code&gt;) rather than deeper levels (like &lt;code&gt;Income:Sales&lt;/code&gt;).&lt;/p&gt;
&lt;p&gt;In the last few lines above, we work out if the reporting period is profitable. If so, we manually create a &lt;code&gt;Profit&lt;/code&gt; account with the left over value so that this can be visualised in the diagram.&lt;/p&gt;
&lt;h2 id=&#34;step-3-producing-the-diagram&#34;&gt;Step 3: Producing the diagram&lt;/h2&gt;
&lt;p&gt;We’ll make use of the &lt;code&gt;matplotlib&lt;/code&gt; package for creating &lt;a href=&#34;https://matplotlib.org/stable/api/sankey_api.html&#34; target=&#34;_blank&#34; rel=&#34;noopener noreferrer&#34; &gt;its own version&lt;/a&gt; of a Sankey diagram. Install this package in your environment (&lt;code&gt;pip install matplotlib&lt;/code&gt;) and then import the required modules at the top of &lt;code&gt;viz.py&lt;/code&gt; (below the &lt;code&gt;sys&lt;/code&gt; import):&lt;/p&gt;
&lt;div class=&#34;highlight&#34;&gt;&lt;pre tabindex=&#34;0&#34; style=&#34;background-color:#f0f3f3;-moz-tab-size:2;-o-tab-size:2;tab-size:2;&#34;&gt;&lt;code class=&#34;language-python&#34; data-lang=&#34;python&#34;&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;&lt;span style=&#34;color:#069;font-weight:bold&#34;&gt;from&lt;/span&gt; &lt;span style=&#34;color:#0cf;font-weight:bold&#34;&gt;matplotlib.sankey&lt;/span&gt; &lt;span style=&#34;color:#069;font-weight:bold&#34;&gt;import&lt;/span&gt; Sankey
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;&lt;span style=&#34;color:#069;font-weight:bold&#34;&gt;import&lt;/span&gt; &lt;span style=&#34;color:#0cf;font-weight:bold&#34;&gt;matplotlib.pyplot&lt;/span&gt; &lt;span style=&#34;color:#069;font-weight:bold&#34;&gt;as&lt;/span&gt; &lt;span style=&#34;color:#0cf;font-weight:bold&#34;&gt;plt&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;Then, at the bottom of your &lt;code&gt;viz.py&lt;/code&gt; file, add the following code:&lt;/p&gt;
&lt;div class=&#34;highlight&#34;&gt;&lt;pre tabindex=&#34;0&#34; style=&#34;background-color:#f0f3f3;-moz-tab-size:2;-o-tab-size:2;tab-size:2;&#34;&gt;&lt;code class=&#34;language-python&#34; data-lang=&#34;python&#34;&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;fig &lt;span style=&#34;color:#555&#34;&gt;=&lt;/span&gt; plt&lt;span style=&#34;color:#555&#34;&gt;.&lt;/span&gt;figure()
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;ax &lt;span style=&#34;color:#555&#34;&gt;=&lt;/span&gt; fig&lt;span style=&#34;color:#555&#34;&gt;.&lt;/span&gt;add_subplot(&lt;span style=&#34;color:#f60&#34;&gt;1&lt;/span&gt;, &lt;span style=&#34;color:#f60&#34;&gt;1&lt;/span&gt;, &lt;span style=&#34;color:#f60&#34;&gt;1&lt;/span&gt;, xticks&lt;span style=&#34;color:#555&#34;&gt;=&lt;/span&gt;[], yticks&lt;span style=&#34;color:#555&#34;&gt;=&lt;/span&gt;[], title&lt;span style=&#34;color:#555&#34;&gt;=&lt;/span&gt;&lt;span style=&#34;color:#c30&#34;&gt;&#34;Ledger Financial Data&#34;&lt;/span&gt;)
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;sankey &lt;span style=&#34;color:#555&#34;&gt;=&lt;/span&gt; Sankey(ax&lt;span style=&#34;color:#555&#34;&gt;=&lt;/span&gt;ax, unit&lt;span style=&#34;color:#555&#34;&gt;=&lt;/span&gt;&lt;span style=&#34;color:#c30&#34;&gt;&#39;£&#39;&lt;/span&gt;)
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;sankey&lt;span style=&#34;color:#555&#34;&gt;.&lt;/span&gt;add(
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;  flows&lt;span style=&#34;color:#555&#34;&gt;=&lt;/span&gt;&lt;span style=&#34;color:#366&#34;&gt;list&lt;/span&gt;(&lt;span style=&#34;color:#366&#34;&gt;map&lt;/span&gt;(&lt;span style=&#34;color:#069;font-weight:bold&#34;&gt;lambda&lt;/span&gt; a: a&lt;span style=&#34;color:#555&#34;&gt;.&lt;/span&gt;value, accounts)),
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;  labels&lt;span style=&#34;color:#555&#34;&gt;=&lt;/span&gt;&lt;span style=&#34;color:#366&#34;&gt;list&lt;/span&gt;(&lt;span style=&#34;color:#366&#34;&gt;map&lt;/span&gt;(&lt;span style=&#34;color:#069;font-weight:bold&#34;&gt;lambda&lt;/span&gt; a: a&lt;span style=&#34;color:#555&#34;&gt;.&lt;/span&gt;name, accounts)),
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;  orientations&lt;span style=&#34;color:#555&#34;&gt;=&lt;/span&gt;&lt;span style=&#34;color:#366&#34;&gt;list&lt;/span&gt;(&lt;span style=&#34;color:#366&#34;&gt;map&lt;/span&gt;(&lt;span style=&#34;color:#069;font-weight:bold&#34;&gt;lambda&lt;/span&gt; a: a&lt;span style=&#34;color:#555&#34;&gt;.&lt;/span&gt;orientation, accounts)),
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;  patchlabel&lt;span style=&#34;color:#555&#34;&gt;=&lt;/span&gt;&lt;span style=&#34;color:#c30&#34;&gt;&#34;Income and expense&#34;&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;)
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;sankey&lt;span style=&#34;color:#555&#34;&gt;.&lt;/span&gt;finish()
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;plt&lt;span style=&#34;color:#555&#34;&gt;.&lt;/span&gt;show()
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;In the above code we instantiate a new figure, axis, and Sankey diagram. We then add a new section to the diagram (using &lt;code&gt;add()&lt;/code&gt;) and to this pass a bunch of parameters:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;code&gt;flows&lt;/code&gt;: a list of total values in each account&lt;/li&gt;
&lt;li&gt;&lt;code&gt;labels&lt;/code&gt;: a list of account names for each account&lt;/li&gt;
&lt;li&gt;&lt;code&gt;orientation&lt;/code&gt;: a list of orientations for the Sankey arrows&lt;/li&gt;
&lt;li&gt;&lt;code&gt;patchlabel&lt;/code&gt;: a title for the Sankey section&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;We then finalise the diagram and ask for the figure to be shown.&lt;/p&gt;
&lt;p&gt;If you now run your code, as described in “Step 1”, you should see something like the following being produced.&lt;/p&gt;
&lt;p&gt;&lt;img src=&#34;https://wilw.dev/media/blog/sankey1.png&#34; alt=&#34;Simple Sankey diagram showing top-level account flows&#34;&gt;&lt;/p&gt;
&lt;p&gt;The diagram shows us some basic information about income, expense, and profit, but it would be nice to get a bit more of a breakdown.&lt;/p&gt;
&lt;h2 id=&#34;step-4-add-sub-accounts-to-the-visualisation&#34;&gt;Step 4: Add sub-accounts to the visualisation&lt;/h2&gt;
&lt;p&gt;To display more of a granular breakdown in the diagram, we need to make changes to the processing step (Step 2) and the display step (Step 3) above.&lt;/p&gt;
&lt;p&gt;To begin with, modify the &lt;code&gt;Account&lt;/code&gt; class so we can provide it with a reference to the account’s parent (if it exists):&lt;/p&gt;
&lt;div class=&#34;highlight&#34;&gt;&lt;pre tabindex=&#34;0&#34; style=&#34;background-color:#f0f3f3;-moz-tab-size:2;-o-tab-size:2;tab-size:2;&#34;&gt;&lt;code class=&#34;language-python&#34; data-lang=&#34;python&#34;&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;&lt;span style=&#34;color:#069;font-weight:bold&#34;&gt;class&lt;/span&gt; &lt;span style=&#34;color:#0a8;font-weight:bold&#34;&gt;Account&lt;/span&gt;:
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;  &lt;span style=&#34;color:#069;font-weight:bold&#34;&gt;def&lt;/span&gt; __init__(self, index, name, value, parent&lt;span style=&#34;color:#555&#34;&gt;=&lt;/span&gt;&lt;span style=&#34;color:#069;font-weight:bold&#34;&gt;None&lt;/span&gt;):
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;    self&lt;span style=&#34;color:#555&#34;&gt;.&lt;/span&gt;name &lt;span style=&#34;color:#555&#34;&gt;=&lt;/span&gt; name
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;    self&lt;span style=&#34;color:#555&#34;&gt;.&lt;/span&gt;value &lt;span style=&#34;color:#555&#34;&gt;=&lt;/span&gt; value
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;    self&lt;span style=&#34;color:#555&#34;&gt;.&lt;/span&gt;parent &lt;span style=&#34;color:#555&#34;&gt;=&lt;/span&gt; parent &lt;span style=&#34;color:#09f;font-style:italic&#34;&gt;# A reference to another Account object&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;    &lt;span style=&#34;color:#555&#34;&gt;...&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;Next, modify the loop in which we create the accounts to detect parent accounts:&lt;/p&gt;
&lt;div class=&#34;highlight&#34;&gt;&lt;pre tabindex=&#34;0&#34; style=&#34;background-color:#f0f3f3;-moz-tab-size:2;-o-tab-size:2;tab-size:2;&#34;&gt;&lt;code class=&#34;language-python&#34; data-lang=&#34;python&#34;&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;accounts &lt;span style=&#34;color:#555&#34;&gt;=&lt;/span&gt; []
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;&lt;span style=&#34;color:#069;font-weight:bold&#34;&gt;for&lt;/span&gt; transaction &lt;span style=&#34;color:#000;font-weight:bold&#34;&gt;in&lt;/span&gt; transactions:
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;  t_accounts &lt;span style=&#34;color:#555&#34;&gt;=&lt;/span&gt; transaction&lt;span style=&#34;color:#555&#34;&gt;.&lt;/span&gt;account&lt;span style=&#34;color:#555&#34;&gt;.&lt;/span&gt;split(&lt;span style=&#34;color:#c30&#34;&gt;&#39;:&#39;&lt;/span&gt;)
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;  &lt;span style=&#34;color:#069;font-weight:bold&#34;&gt;for&lt;/span&gt; level, a &lt;span style=&#34;color:#000;font-weight:bold&#34;&gt;in&lt;/span&gt; &lt;span style=&#34;color:#366&#34;&gt;enumerate&lt;/span&gt;(t_accounts):
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;    account_name &lt;span style=&#34;color:#555&#34;&gt;=&lt;/span&gt; &lt;span style=&#34;color:#c30&#34;&gt;&#39;:&#39;&lt;/span&gt;&lt;span style=&#34;color:#555&#34;&gt;.&lt;/span&gt;join(t_accounts[:level &lt;span style=&#34;color:#555&#34;&gt;&#43;&lt;/span&gt; &lt;span style=&#34;color:#f60&#34;&gt;1&lt;/span&gt;])
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;    parent_account_name &lt;span style=&#34;color:#555&#34;&gt;=&lt;/span&gt; &lt;span style=&#34;color:#c30&#34;&gt;&#39;:&#39;&lt;/span&gt;&lt;span style=&#34;color:#555&#34;&gt;.&lt;/span&gt;join(t_accounts[:level])
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;    account &lt;span style=&#34;color:#555&#34;&gt;=&lt;/span&gt; &lt;span style=&#34;color:#366&#34;&gt;next&lt;/span&gt;(&lt;span style=&#34;color:#366&#34;&gt;filter&lt;/span&gt;(&lt;span style=&#34;color:#069;font-weight:bold&#34;&gt;lambda&lt;/span&gt; a: a&lt;span style=&#34;color:#555&#34;&gt;.&lt;/span&gt;name &lt;span style=&#34;color:#555&#34;&gt;==&lt;/span&gt; account_name, accounts), &lt;span style=&#34;color:#069;font-weight:bold&#34;&gt;None&lt;/span&gt;)
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;    &lt;span style=&#34;color:#069;font-weight:bold&#34;&gt;if&lt;/span&gt; &lt;span style=&#34;color:#000;font-weight:bold&#34;&gt;not&lt;/span&gt; account:
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;      parent_account &lt;span style=&#34;color:#555&#34;&gt;=&lt;/span&gt; &lt;span style=&#34;color:#069;font-weight:bold&#34;&gt;None&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;      &lt;span style=&#34;color:#069;font-weight:bold&#34;&gt;if&lt;/span&gt; parent_account_name:
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;        parent_account &lt;span style=&#34;color:#555&#34;&gt;=&lt;/span&gt; &lt;span style=&#34;color:#366&#34;&gt;next&lt;/span&gt;(&lt;span style=&#34;color:#366&#34;&gt;filter&lt;/span&gt;(&lt;span style=&#34;color:#069;font-weight:bold&#34;&gt;lambda&lt;/span&gt; a: a&lt;span style=&#34;color:#555&#34;&gt;.&lt;/span&gt;name &lt;span style=&#34;color:#555&#34;&gt;==&lt;/span&gt; parent_account_name, accounts), &lt;span style=&#34;color:#069;font-weight:bold&#34;&gt;None&lt;/span&gt;)
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;      account &lt;span style=&#34;color:#555&#34;&gt;=&lt;/span&gt; Account(&lt;span style=&#34;color:#366&#34;&gt;len&lt;/span&gt;(accounts), account_name, &lt;span style=&#34;color:#f60&#34;&gt;0&lt;/span&gt;, parent_account)
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;      accounts&lt;span style=&#34;color:#555&#34;&gt;.&lt;/span&gt;append(account)
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;    account&lt;span style=&#34;color:#555&#34;&gt;.&lt;/span&gt;value &lt;span style=&#34;color:#555&#34;&gt;&#43;=&lt;/span&gt; transaction&lt;span style=&#34;color:#555&#34;&gt;.&lt;/span&gt;value
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;The code above performs various additional lookups to try and find parent accounts, and instantiates all accounts at each level in the Ledger file.&lt;/p&gt;
&lt;p&gt;Finally, we can adjust the diagram-producing code to make use of the various accounts:&lt;/p&gt;
&lt;div class=&#34;highlight&#34;&gt;&lt;pre tabindex=&#34;0&#34; style=&#34;background-color:#f0f3f3;-moz-tab-size:2;-o-tab-size:2;tab-size:2;&#34;&gt;&lt;code class=&#34;language-python&#34; data-lang=&#34;python&#34;&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;sankey &lt;span style=&#34;color:#555&#34;&gt;=&lt;/span&gt; Sankey(ax&lt;span style=&#34;color:#555&#34;&gt;=&lt;/span&gt;ax, unit&lt;span style=&#34;color:#555&#34;&gt;=&lt;/span&gt;&lt;span style=&#34;color:#c30&#34;&gt;&#39;£&#39;&lt;/span&gt;)
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;&lt;span style=&#34;color:#069;font-weight:bold&#34;&gt;for&lt;/span&gt; parent_account &lt;span style=&#34;color:#000;font-weight:bold&#34;&gt;in&lt;/span&gt; &lt;span style=&#34;color:#366&#34;&gt;filter&lt;/span&gt;(&lt;span style=&#34;color:#069;font-weight:bold&#34;&gt;lambda&lt;/span&gt; a: &lt;span style=&#34;color:#000;font-weight:bold&#34;&gt;not&lt;/span&gt; a&lt;span style=&#34;color:#555&#34;&gt;.&lt;/span&gt;parent, accounts):
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;  children &lt;span style=&#34;color:#555&#34;&gt;=&lt;/span&gt; &lt;span style=&#34;color:#366&#34;&gt;list&lt;/span&gt;(&lt;span style=&#34;color:#366&#34;&gt;filter&lt;/span&gt;(&lt;span style=&#34;color:#069;font-weight:bold&#34;&gt;lambda&lt;/span&gt; a: (a&lt;span style=&#34;color:#555&#34;&gt;.&lt;/span&gt;parent &lt;span style=&#34;color:#000;font-weight:bold&#34;&gt;and&lt;/span&gt; a&lt;span style=&#34;color:#555&#34;&gt;.&lt;/span&gt;parent&lt;span style=&#34;color:#555&#34;&gt;.&lt;/span&gt;name) &lt;span style=&#34;color:#555&#34;&gt;==&lt;/span&gt; parent_account&lt;span style=&#34;color:#555&#34;&gt;.&lt;/span&gt;name, accounts))
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;  &lt;span style=&#34;color:#069;font-weight:bold&#34;&gt;if&lt;/span&gt; &lt;span style=&#34;color:#366&#34;&gt;len&lt;/span&gt;(children):
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;    sankey&lt;span style=&#34;color:#555&#34;&gt;.&lt;/span&gt;add(
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;      flows&lt;span style=&#34;color:#555&#34;&gt;=&lt;/span&gt;&lt;span style=&#34;color:#366&#34;&gt;list&lt;/span&gt;(&lt;span style=&#34;color:#366&#34;&gt;map&lt;/span&gt;(&lt;span style=&#34;color:#069;font-weight:bold&#34;&gt;lambda&lt;/span&gt; a: a&lt;span style=&#34;color:#555&#34;&gt;.&lt;/span&gt;value, children)),
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;      labels&lt;span style=&#34;color:#555&#34;&gt;=&lt;/span&gt;&lt;span style=&#34;color:#366&#34;&gt;list&lt;/span&gt;(&lt;span style=&#34;color:#366&#34;&gt;map&lt;/span&gt;(&lt;span style=&#34;color:#069;font-weight:bold&#34;&gt;lambda&lt;/span&gt; a: a&lt;span style=&#34;color:#555&#34;&gt;.&lt;/span&gt;name, children)),
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;      orientations&lt;span style=&#34;color:#555&#34;&gt;=&lt;/span&gt;&lt;span style=&#34;color:#366&#34;&gt;list&lt;/span&gt;(&lt;span style=&#34;color:#366&#34;&gt;map&lt;/span&gt;(&lt;span style=&#34;color:#069;font-weight:bold&#34;&gt;lambda&lt;/span&gt; a: a&lt;span style=&#34;color:#555&#34;&gt;.&lt;/span&gt;orientation, children)),
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;    )
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;  &lt;span style=&#34;color:#069;font-weight:bold&#34;&gt;else&lt;/span&gt;:
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;    sankey&lt;span style=&#34;color:#555&#34;&gt;.&lt;/span&gt;add(
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;      flows&lt;span style=&#34;color:#555&#34;&gt;=&lt;/span&gt;[parent_account&lt;span style=&#34;color:#555&#34;&gt;.&lt;/span&gt;value],
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;      labels&lt;span style=&#34;color:#555&#34;&gt;=&lt;/span&gt;[parent_account&lt;span style=&#34;color:#555&#34;&gt;.&lt;/span&gt;name],
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;      orientations&lt;span style=&#34;color:#555&#34;&gt;=&lt;/span&gt;[parent_account&lt;span style=&#34;color:#555&#34;&gt;.&lt;/span&gt;orientation],
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;    )
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;sankey&lt;span style=&#34;color:#555&#34;&gt;.&lt;/span&gt;finish()
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;plt&lt;span style=&#34;color:#555&#34;&gt;.&lt;/span&gt;show()
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;This code now iterates through each parent account (i.e. each account without a parent of its own). It then looks up the children of these accounts and creates a new Sankey segment containing the child accounts (or a single account for the parent if there are no child accounts).&lt;/p&gt;
&lt;p&gt;Running the script again with the Ledger data should yield something like the following diagram.&lt;/p&gt;
&lt;p&gt;&lt;img src=&#34;https://wilw.dev/media/blog/sankey2.png&#34; alt=&#34;More useful Sankey diagram showing an additional account level flow&#34;&gt;&lt;/p&gt;
&lt;p&gt;We now get more useful information being displayed, and we can see - at a glance - where the money is going.&lt;/p&gt;
&lt;h2 id=&#34;conclusion&#34;&gt;Conclusion&lt;/h2&gt;
&lt;p&gt;Although we have made improvements to the diagram, there are still possible further enhancements that you may wish to explore, depending on your needs. For example, the current approach only goes two levels deep; the display stage would need to be amended to recursively navigate account levels to get an even finer-grained view.&lt;/p&gt;
&lt;p&gt;I can recommend taking a look through the &lt;code&gt;matplotlib&lt;/code&gt; &lt;a href=&#34;https://matplotlib.org/stable/api/sankey_api.html&#34; target=&#34;_blank&#34; rel=&#34;noopener noreferrer&#34; &gt;Sankey documentation&lt;/a&gt; for more information on how you can tweak the diagram, or the &lt;a href=&#34;https://matplotlib.org/stable/api&#34; target=&#34;_blank&#34; rel=&#34;noopener noreferrer&#34; &gt;documentation in general&lt;/a&gt; should you wish to explore other types of visualisations that you can pass the processed transactions in to.&lt;/p&gt;
</content:encoded>
    </item>
    
    <item>
      <title>On alcohol</title>
      <link>https://wilw.dev/blog/2022/04/12/alcohol/</link>
      <pubDate>Tue, 12 Apr 2022 17:55:00 +0000</pubDate>
      
      <guid>https://wilw.dev/blog/2022/04/12/alcohol/</guid>
      
        <category>life</category>
      
      
      <content:encoded>&lt;p&gt;Whilst my days of binge drinking as a student are thankfully far in my past, alcohol is still an ongoing, yet much more minor, part of my life.&lt;/p&gt;
&lt;p&gt;Like many millennials (and I’m sure it must be the same for other generations too), we got used to it as a mechanism for socialising. Whether this is meeting friends after work, going out for dinner with family, or spending time in the pub or at home with a significant other.&lt;/p&gt;
&lt;p&gt;Throughout my life I’ve been lucky enough to meet or work with people who just don’t drink. And although it’s taken me a while, I’m certainly starting to see why. The thought of feeling that you &lt;em&gt;have&lt;/em&gt; to drink in order to enjoy one’s self is clearly ridiculous, and if someone does find this to be the case then it may be a sign of more fundamental issues.&lt;/p&gt;
&lt;p&gt;Whilst I understand and appreciate the relaxing effect of alcohol, I’ve recently begun to wonder about whether this outweighs the down-sides. Some of the negative effects I’ve begun to personally notice after consuming alcohol, or related in general, are;&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;Sleep:&lt;/strong&gt; my sleep quality (and subsequent potential the following day) is significantly reduced. It’s known that alcohol can inhibit the brain’s ability to “deep sleep” even if you appear to be asleep.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Fitness:&lt;/strong&gt; my workouts are less impactful, and things become harder than they should. This inhibits progression.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Weight:&lt;/strong&gt; generally, alcohol is calorific and does not fill you up like food does.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Hangovers:&lt;/strong&gt; whilst only after a (thankfully) less-frequent particularly heavy night, these just cancel out the next day. Even “light” hangovers can severely impact morning productivity. These are also accompanied by depression, shame, and other negative emotions I don’t need or want.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Mindfulness:&lt;/strong&gt; my mood, ability to deep think, and motivation are impacted.&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;There are also things I can’t observe directly myself that I am sure must be affected, such as the effect on my health (both physical and mental).&lt;/p&gt;
&lt;p&gt;I don’t intend to give it up entirely (it’s always nice for celebrating with others or for special occasions), but I’m going to spend at least a few weeks alcohol-free and see how things go with social events and seeing family. It will alter my routine a little (e.g. instead of meeting friends in the pub I will suggest coffee instead), and perhaps it’ll change the way others in my peer and family group see me, but I’m excited to hopefully see and feel some day-to-day benefits that will enable me to be more informed in weighing-up some of my decisions going forward.&lt;/p&gt;
</content:encoded>
    </item>
    
    <item>
      <title>Self-hosting apps and services using Traefik reverse proxy</title>
      <link>https://wilw.dev/blog/2022/04/09/nginx-traefik-selfhosted/</link>
      <pubDate>Sat, 09 Apr 2022 13:22:00 +0000</pubDate>
      
      <guid>https://wilw.dev/blog/2022/04/09/nginx-traefik-selfhosted/</guid>
      
        <category>technology</category>
      
        <category>selfhost</category>
      
      
      <content:encoded>&lt;p&gt;I often talk about self-hosting on this blog, and I’m certainly a big fan of being able to control my own data and systems wherever possible (and feasible). I’ve recently switched from using Nginx to Traefik as a reverse proxy for my server and for terminating TLS connections.&lt;/p&gt;
&lt;p&gt;In this post I’ll talk a little about why and how I made this change.&lt;/p&gt;
&lt;h2 id=&#34;my-until-recent-setup&#34;&gt;My (until recent) setup&lt;/h2&gt;
&lt;p&gt;I self-host a number of services; including &lt;a href=&#34;https://www.nextcloud.com&#34; target=&#34;_blank&#34; rel=&#34;noopener noreferrer&#34; &gt;Nextcloud&lt;/a&gt; for file storage and sync, &lt;a href=&#34;https://gitea.io&#34; target=&#34;_blank&#34; rel=&#34;noopener noreferrer&#34; &gt;Gitea&lt;/a&gt; for git syncing, &lt;a href=&#34;https://www.freshrss.org&#34; target=&#34;_blank&#34; rel=&#34;noopener noreferrer&#34; &gt;FreshRSS&lt;/a&gt; for RSS feed aggregation and management, &lt;a href=&#34;https://github.com/monicahq/monica&#34; target=&#34;_blank&#34; rel=&#34;noopener noreferrer&#34; &gt;Monica&lt;/a&gt; for relationship organisation, and a few other things too.&lt;/p&gt;
&lt;p&gt;To date, I’ve mostly relied on &lt;a href=&#34;https://www.nginx.com&#34; target=&#34;_blank&#34; rel=&#34;noopener noreferrer&#34; &gt;Nginx&lt;/a&gt; for providing a reverse proxy, enabling me to run many of these services on a single VPS. I run everything, including Nginx, using Docker containers, which I find much more convenient and easy to manage.&lt;/p&gt;
&lt;p&gt;Individual services are managed using &lt;a href=&#34;https://docs.docker.com/compose&#34; target=&#34;_blank&#34; rel=&#34;noopener noreferrer&#34; &gt;Docker Compose&lt;/a&gt;, which allows me to keep services that rely on multiple containers (such as those that depend on both a web server and a backend database server) logically separated and organised. I use a single &lt;code&gt;nginx.conf&lt;/code&gt; file, used by my Nginx container, to manage these services as virtual hosts and for configuring TLS certificates.&lt;/p&gt;
&lt;h2 id=&#34;why-i-wanted-a-change&#34;&gt;Why I wanted a change&lt;/h2&gt;
&lt;p&gt;This setup works well and has served me well for several years. However, it isn’t without its drawbacks;&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;The basic system relies on manual and semi-tedious config entries to my &lt;code&gt;nginx.conf&lt;/code&gt;, including HTTP -&gt; HTTPS redirects, enumerating virtual hosts, and other deeper Nginx stuff I don’t always fully understand.&lt;/li&gt;
&lt;li&gt;TLS certificate provisioning and renewal: I either need to do this manually or remember to setup (and then trust) a cron job to renew things properly.&lt;/li&gt;
&lt;li&gt;Domain verification for Let’s Encrypt: configuring webroots and interception of ACME requests for each virtual host.&lt;/li&gt;
&lt;li&gt;Needing to restart (or automate the restart) of the webserver after receiving new certificates.&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;Most of these pain points are related to the reverse proxy setup itself, rather than the individual services (which can happily run indefinitely). To me, it felt like the webserver side of things should be the most “boring” and least time-consuming, when in reality it is the opposite.&lt;/p&gt;
&lt;p&gt;I wanted to find a new solution that would help me solve these issues and allow me to focus more on maintaining and working on the services themselves.&lt;/p&gt;
&lt;p&gt;## Traefik as a reverse proxy&lt;/p&gt;
&lt;p&gt;&lt;a href=&#34;https://traefik.io&#34; target=&#34;_blank&#34; rel=&#34;noopener noreferrer&#34; &gt;Traefik&lt;/a&gt; provides a &lt;a href=&#34;https://traefik.io/traefik&#34; target=&#34;_blank&#34; rel=&#34;noopener noreferrer&#34; &gt;proxy system&lt;/a&gt; that I’ve used before in Kubernetes setups. It acts as a reverse proxy, TLS terminator, load balancer, and much more. It’s designed to work well with microservices running in containers, and whilst Nginx can certainly achieve all of this too, the nice thing about Traefik is that it does this for you &lt;strong&gt;dynamically and automatically&lt;/strong&gt;.&lt;/p&gt;
&lt;p&gt;&lt;img src=&#34;https://wilw.dev/media/blog/traefik.png&#34; alt=&#34;Traefik logo&#34;&gt;&lt;/p&gt;
&lt;p&gt;It can easily be configured using Docker Compose, and all the TLS provisioning is handled automatically via Docker &lt;em&gt;labels&lt;/em&gt;. The &lt;a href=&#34;https://doc.traefik.io/traefik&#34; target=&#34;_blank&#34; rel=&#34;noopener noreferrer&#34; &gt;documentation&lt;/a&gt; covers the setup well and in detail, but below I will run through (roughly) how I made the switch from Nginx to Traefik.&lt;/p&gt;
&lt;p&gt;### Step 1: Firstly, stop all of your running services&lt;/p&gt;
&lt;p&gt;If you use Compose, you can simply run &lt;code&gt;docker-compose down&lt;/code&gt; to take your services down. At the very least, make sure you are shutting down your Nginx container.&lt;/p&gt;
&lt;p&gt;&lt;em&gt;Note: If your user is not in the &lt;code&gt;docker&lt;/code&gt; group you may need to use &lt;code&gt;sudo&lt;/code&gt; to run &lt;code&gt;docker-compose&lt;/code&gt; commands.&lt;/em&gt;&lt;/p&gt;
&lt;h3 id=&#34;step-2-next-write-a-new-docker-compose-file&#34;&gt;Step 2: Next, write a new Docker Compose file&lt;/h3&gt;
&lt;p&gt;Create a new directory to house your Traefik setup, and in here write a new &lt;code&gt;docker-compose.yml&lt;/code&gt; file, as shown below. In this example I am using Traefik to route requests through to Nextcloud and FreshRSS instances:&lt;/p&gt;
&lt;div class=&#34;highlight&#34;&gt;&lt;pre tabindex=&#34;0&#34; style=&#34;background-color:#f0f3f3;-moz-tab-size:2;-o-tab-size:2;tab-size:2;&#34;&gt;&lt;code class=&#34;language-docker&#34; data-lang=&#34;docker&#34;&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;version: &lt;span style=&#34;color:#c30&#34;&gt;&#39;3&#39;&lt;/span&gt;&lt;span style=&#34;color:#a00;background-color:#faa&#34;&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;&lt;span style=&#34;color:#a00;background-color:#faa&#34;&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;&lt;span style=&#34;color:#a00;background-color:#faa&#34;&gt;&lt;/span&gt;services:&lt;span style=&#34;color:#a00;background-color:#faa&#34;&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;&lt;span style=&#34;color:#a00;background-color:#faa&#34;&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;&lt;span style=&#34;color:#a00;background-color:#faa&#34;&gt;&lt;/span&gt;  &lt;span style=&#34;color:#09f;font-style:italic&#34;&gt;# The Traefik service&lt;/span&gt;&lt;span style=&#34;color:#a00;background-color:#faa&#34;&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;&lt;span style=&#34;color:#a00;background-color:#faa&#34;&gt;&lt;/span&gt;  reverse-proxy:&lt;span style=&#34;color:#a00;background-color:#faa&#34;&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;&lt;span style=&#34;color:#a00;background-color:#faa&#34;&gt;&lt;/span&gt;    image: traefik:v2.6&lt;span style=&#34;color:#a00;background-color:#faa&#34;&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;&lt;span style=&#34;color:#a00;background-color:#faa&#34;&gt;&lt;/span&gt;    command:&lt;span style=&#34;color:#a00;background-color:#faa&#34;&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;&lt;span style=&#34;color:#a00;background-color:#faa&#34;&gt;&lt;/span&gt;      &lt;span style=&#34;color:#09f;font-style:italic&#34;&gt;# Tell Traefik that we&#39;re working in a Docker environment&lt;/span&gt;&lt;span style=&#34;color:#a00;background-color:#faa&#34;&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;&lt;span style=&#34;color:#a00;background-color:#faa&#34;&gt;&lt;/span&gt;      &lt;span style=&#34;color:#09f;font-style:italic&#34;&gt;# This allows it to dynamically pull out the configuration&lt;/span&gt;&lt;span style=&#34;color:#a00;background-color:#faa&#34;&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;&lt;span style=&#34;color:#a00;background-color:#faa&#34;&gt;&lt;/span&gt;      - &lt;span style=&#34;color:#c30&#34;&gt;&#34;--providers.docker&#34;&lt;/span&gt;&lt;span style=&#34;color:#a00;background-color:#faa&#34;&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;&lt;span style=&#34;color:#a00;background-color:#faa&#34;&gt;&lt;/span&gt;      &lt;span style=&#34;color:#09f;font-style:italic&#34;&gt;# Standard ports for HTTP/S&lt;/span&gt;&lt;span style=&#34;color:#a00;background-color:#faa&#34;&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;&lt;span style=&#34;color:#a00;background-color:#faa&#34;&gt;&lt;/span&gt;      - &lt;span style=&#34;color:#c30&#34;&gt;&#34;--entrypoints.web.address=:80&#34;&lt;/span&gt;&lt;span style=&#34;color:#a00;background-color:#faa&#34;&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;&lt;span style=&#34;color:#a00;background-color:#faa&#34;&gt;&lt;/span&gt;      - &lt;span style=&#34;color:#c30&#34;&gt;&#34;--entrypoints.websecure.address=:443&#34;&lt;/span&gt;&lt;span style=&#34;color:#a00;background-color:#faa&#34;&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;&lt;span style=&#34;color:#a00;background-color:#faa&#34;&gt;&lt;/span&gt;      &lt;span style=&#34;color:#09f;font-style:italic&#34;&gt;# ACME TLS certificate resolver setup. Remember to change your email address here&lt;/span&gt;&lt;span style=&#34;color:#a00;background-color:#faa&#34;&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;&lt;span style=&#34;color:#a00;background-color:#faa&#34;&gt;&lt;/span&gt;      - &lt;span style=&#34;color:#c30&#34;&gt;&#34;--certificatesresolvers.myresolver.acme.email=me@example.com&#34;&lt;/span&gt;&lt;span style=&#34;color:#a00;background-color:#faa&#34;&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;&lt;span style=&#34;color:#a00;background-color:#faa&#34;&gt;&lt;/span&gt;      - &lt;span style=&#34;color:#c30&#34;&gt;&#34;--certificatesresolvers.myresolver.acme.storage=/etc/traefik/acme/acme.json&#34;&lt;/span&gt;&lt;span style=&#34;color:#a00;background-color:#faa&#34;&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;&lt;span style=&#34;color:#a00;background-color:#faa&#34;&gt;&lt;/span&gt;      - &lt;span style=&#34;color:#c30&#34;&gt;&#34;--certificatesresolvers.myresolver.acme.httpchallenge.entrypoint=web&#34;&lt;/span&gt;&lt;span style=&#34;color:#a00;background-color:#faa&#34;&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;&lt;span style=&#34;color:#a00;background-color:#faa&#34;&gt;&lt;/span&gt;    ports:&lt;span style=&#34;color:#a00;background-color:#faa&#34;&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;&lt;span style=&#34;color:#a00;background-color:#faa&#34;&gt;&lt;/span&gt;      &lt;span style=&#34;color:#09f;font-style:italic&#34;&gt;# Map our web traffic ports to the host network&lt;/span&gt;&lt;span style=&#34;color:#a00;background-color:#faa&#34;&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;&lt;span style=&#34;color:#a00;background-color:#faa&#34;&gt;&lt;/span&gt;      - &lt;span style=&#34;color:#c30&#34;&gt;&#34;80:80&#34;&lt;/span&gt;&lt;span style=&#34;color:#a00;background-color:#faa&#34;&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;&lt;span style=&#34;color:#a00;background-color:#faa&#34;&gt;&lt;/span&gt;      - &lt;span style=&#34;color:#c30&#34;&gt;&#34;443:443&#34;&lt;/span&gt;&lt;span style=&#34;color:#a00;background-color:#faa&#34;&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;&lt;span style=&#34;color:#a00;background-color:#faa&#34;&gt;&lt;/span&gt;    volumes:&lt;span style=&#34;color:#a00;background-color:#faa&#34;&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;&lt;span style=&#34;color:#a00;background-color:#faa&#34;&gt;&lt;/span&gt;      &lt;span style=&#34;color:#09f;font-style:italic&#34;&gt;# Mounting this volume is important to ensure certificates can be persisted&lt;/span&gt;&lt;span style=&#34;color:#a00;background-color:#faa&#34;&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;&lt;span style=&#34;color:#a00;background-color:#faa&#34;&gt;&lt;/span&gt;      - ./acme:/etc/traefik/acme&lt;span style=&#34;color:#a00;background-color:#faa&#34;&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;&lt;span style=&#34;color:#a00;background-color:#faa&#34;&gt;&lt;/span&gt;      &lt;span style=&#34;color:#09f;font-style:italic&#34;&gt;# This volume is to enable Traefik to communicate with Docker&lt;/span&gt;&lt;span style=&#34;color:#a00;background-color:#faa&#34;&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;&lt;span style=&#34;color:#a00;background-color:#faa&#34;&gt;&lt;/span&gt;      - /var/run/docker.sock:/var/run/docker.sock&lt;span style=&#34;color:#a00;background-color:#faa&#34;&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;&lt;span style=&#34;color:#a00;background-color:#faa&#34;&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;&lt;span style=&#34;color:#a00;background-color:#faa&#34;&gt;&lt;/span&gt;  &lt;span style=&#34;color:#09f;font-style:italic&#34;&gt;# The FreshRSS service&lt;/span&gt;&lt;span style=&#34;color:#a00;background-color:#faa&#34;&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;&lt;span style=&#34;color:#a00;background-color:#faa&#34;&gt;&lt;/span&gt;  &lt;span style=&#34;color:#09f;font-style:italic&#34;&gt;# (see https://github.com/FreshRSS/FreshRSS/tree/edge/Docker for more info)&lt;/span&gt;&lt;span style=&#34;color:#a00;background-color:#faa&#34;&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;&lt;span style=&#34;color:#a00;background-color:#faa&#34;&gt;&lt;/span&gt;  freshrss:&lt;span style=&#34;color:#a00;background-color:#faa&#34;&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;&lt;span style=&#34;color:#a00;background-color:#faa&#34;&gt;&lt;/span&gt;    image: freshrss/freshrss&lt;span style=&#34;color:#a00;background-color:#faa&#34;&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;&lt;span style=&#34;color:#a00;background-color:#faa&#34;&gt;&lt;/span&gt;    environment:&lt;span style=&#34;color:#a00;background-color:#faa&#34;&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;&lt;span style=&#34;color:#a00;background-color:#faa&#34;&gt;&lt;/span&gt;      - &lt;span style=&#34;color:#c30&#34;&gt;&#34;CRON_MIN=3,33&#34;&lt;/span&gt;&lt;span style=&#34;color:#a00;background-color:#faa&#34;&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;&lt;span style=&#34;color:#a00;background-color:#faa&#34;&gt;&lt;/span&gt;      - &lt;span style=&#34;color:#c30&#34;&gt;&#34;TZ=Europe/London&#34;&lt;/span&gt;&lt;span style=&#34;color:#a00;background-color:#faa&#34;&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;&lt;span style=&#34;color:#a00;background-color:#faa&#34;&gt;&lt;/span&gt;    volumes:&lt;span style=&#34;color:#a00;background-color:#faa&#34;&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;&lt;span style=&#34;color:#a00;background-color:#faa&#34;&gt;&lt;/span&gt;      - ./freshrss_data:/var/www/FreshRSS/data&lt;span style=&#34;color:#a00;background-color:#faa&#34;&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;&lt;span style=&#34;color:#a00;background-color:#faa&#34;&gt;&lt;/span&gt;      - ./freshrss_extensions:/var/www/FreshRSS/extensions&lt;span style=&#34;color:#a00;background-color:#faa&#34;&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;&lt;span style=&#34;color:#a00;background-color:#faa&#34;&gt;&lt;/span&gt;    labels:&lt;span style=&#34;color:#a00;background-color:#faa&#34;&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;&lt;span style=&#34;color:#a00;background-color:#faa&#34;&gt;&lt;/span&gt;      &lt;span style=&#34;color:#09f;font-style:italic&#34;&gt;# Here we add a label to tell Traefik how to route to this service by domain&lt;/span&gt;&lt;span style=&#34;color:#a00;background-color:#faa&#34;&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;&lt;span style=&#34;color:#a00;background-color:#faa&#34;&gt;&lt;/span&gt;      &lt;span style=&#34;color:#09f;font-style:italic&#34;&gt;# Remember to change this host value.&lt;/span&gt;&lt;span style=&#34;color:#a00;background-color:#faa&#34;&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;&lt;span style=&#34;color:#a00;background-color:#faa&#34;&gt;&lt;/span&gt;      - traefik.http.routers.freshrss.rule&lt;span style=&#34;color:#555&#34;&gt;=&lt;/span&gt;Host&lt;span style=&#34;color:#555&#34;&gt;(&lt;/span&gt;&lt;span style=&#34;color:#c30&#34;&gt;`&lt;/span&gt;rss.example.com&lt;span style=&#34;color:#c30&#34;&gt;`&lt;/span&gt;&lt;span style=&#34;color:#555&#34;&gt;)&lt;/span&gt;&lt;span style=&#34;color:#a00;background-color:#faa&#34;&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;&lt;span style=&#34;color:#a00;background-color:#faa&#34;&gt;&lt;/span&gt;      &lt;span style=&#34;color:#09f;font-style:italic&#34;&gt;# These two labels tell Traefik to setup TLS for this service&lt;/span&gt;&lt;span style=&#34;color:#a00;background-color:#faa&#34;&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;&lt;span style=&#34;color:#a00;background-color:#faa&#34;&gt;&lt;/span&gt;      - traefik.http.routers.freshrss.tls&lt;span style=&#34;color:#555&#34;&gt;=&lt;/span&gt;true&lt;span style=&#34;color:#a00;background-color:#faa&#34;&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;&lt;span style=&#34;color:#a00;background-color:#faa&#34;&gt;&lt;/span&gt;      - traefik.http.routers.freshrss.tls.certresolver&lt;span style=&#34;color:#555&#34;&gt;=&lt;/span&gt;myresolver&lt;span style=&#34;color:#a00;background-color:#faa&#34;&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;&lt;span style=&#34;color:#a00;background-color:#faa&#34;&gt;&lt;/span&gt;    restart: always&lt;span style=&#34;color:#a00;background-color:#faa&#34;&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;&lt;span style=&#34;color:#a00;background-color:#faa&#34;&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;&lt;span style=&#34;color:#a00;background-color:#faa&#34;&gt;&lt;/span&gt;  &lt;span style=&#34;color:#09f;font-style:italic&#34;&gt;# The Nextcloud service&lt;/span&gt;&lt;span style=&#34;color:#a00;background-color:#faa&#34;&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;&lt;span style=&#34;color:#a00;background-color:#faa&#34;&gt;&lt;/span&gt;  &lt;span style=&#34;color:#09f;font-style:italic&#34;&gt;# (see https://hub.docker.com/_/nextcloud for more info)&lt;/span&gt;&lt;span style=&#34;color:#a00;background-color:#faa&#34;&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;&lt;span style=&#34;color:#a00;background-color:#faa&#34;&gt;&lt;/span&gt;  nextclouddb:&lt;span style=&#34;color:#a00;background-color:#faa&#34;&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;&lt;span style=&#34;color:#a00;background-color:#faa&#34;&gt;&lt;/span&gt;    image: mariadb:10.5&lt;span style=&#34;color:#a00;background-color:#faa&#34;&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;&lt;span style=&#34;color:#a00;background-color:#faa&#34;&gt;&lt;/span&gt;    command: --transaction-isolation&lt;span style=&#34;color:#555&#34;&gt;=&lt;/span&gt;READ-COMMITTED --binlog-format&lt;span style=&#34;color:#555&#34;&gt;=&lt;/span&gt;ROW&lt;span style=&#34;color:#a00;background-color:#faa&#34;&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;&lt;span style=&#34;color:#a00;background-color:#faa&#34;&gt;&lt;/span&gt;    restart: always&lt;span style=&#34;color:#a00;background-color:#faa&#34;&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;&lt;span style=&#34;color:#a00;background-color:#faa&#34;&gt;&lt;/span&gt;    volumes:&lt;span style=&#34;color:#a00;background-color:#faa&#34;&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;&lt;span style=&#34;color:#a00;background-color:#faa&#34;&gt;&lt;/span&gt;      - ./nextcloud-data/db:/var/lib/mysql&lt;span style=&#34;color:#a00;background-color:#faa&#34;&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;&lt;span style=&#34;color:#a00;background-color:#faa&#34;&gt;&lt;/span&gt;    environment:&lt;span style=&#34;color:#a00;background-color:#faa&#34;&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;&lt;span style=&#34;color:#a00;background-color:#faa&#34;&gt;&lt;/span&gt;      &lt;span style=&#34;color:#09f;font-style:italic&#34;&gt;# Remember to change these values:&lt;/span&gt;&lt;span style=&#34;color:#a00;background-color:#faa&#34;&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;&lt;span style=&#34;color:#a00;background-color:#faa&#34;&gt;&lt;/span&gt;      - &lt;span style=&#34;color:#033&#34;&gt;MYSQL_ROOT_PASSWORD&lt;/span&gt;&lt;span style=&#34;color:#555&#34;&gt;=&lt;/span&gt;&lt;PASSWORD&gt;&lt;span style=&#34;color:#a00;background-color:#faa&#34;&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;&lt;span style=&#34;color:#a00;background-color:#faa&#34;&gt;&lt;/span&gt;      - &lt;span style=&#34;color:#033&#34;&gt;MYSQL_PASSWORD&lt;/span&gt;&lt;span style=&#34;color:#555&#34;&gt;=&lt;/span&gt;&lt;PASSWORD&gt;&lt;span style=&#34;color:#a00;background-color:#faa&#34;&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;&lt;span style=&#34;color:#a00;background-color:#faa&#34;&gt;&lt;/span&gt;      - &lt;span style=&#34;color:#033&#34;&gt;MYSQL_DATABASE&lt;/span&gt;&lt;span style=&#34;color:#555&#34;&gt;=&lt;/span&gt;nextcloud&lt;span style=&#34;color:#a00;background-color:#faa&#34;&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;&lt;span style=&#34;color:#a00;background-color:#faa&#34;&gt;&lt;/span&gt;      - &lt;span style=&#34;color:#033&#34;&gt;MYSQL_USER&lt;/span&gt;&lt;span style=&#34;color:#555&#34;&gt;=&lt;/span&gt;nextcloud&lt;span style=&#34;color:#a00;background-color:#faa&#34;&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;&lt;span style=&#34;color:#a00;background-color:#faa&#34;&gt;&lt;/span&gt;  nextcloud:&lt;span style=&#34;color:#a00;background-color:#faa&#34;&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;&lt;span style=&#34;color:#a00;background-color:#faa&#34;&gt;&lt;/span&gt;    image: nextcloud:22&lt;span style=&#34;color:#a00;background-color:#faa&#34;&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;&lt;span style=&#34;color:#a00;background-color:#faa&#34;&gt;&lt;/span&gt;    volumes:&lt;span style=&#34;color:#a00;background-color:#faa&#34;&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;&lt;span style=&#34;color:#a00;background-color:#faa&#34;&gt;&lt;/span&gt;      - ./nextcloud-data/storage:/var/www/html&lt;span style=&#34;color:#a00;background-color:#faa&#34;&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;&lt;span style=&#34;color:#a00;background-color:#faa&#34;&gt;&lt;/span&gt;    restart: always&lt;span style=&#34;color:#a00;background-color:#faa&#34;&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;&lt;span style=&#34;color:#a00;background-color:#faa&#34;&gt;&lt;/span&gt;    labels:&lt;span style=&#34;color:#a00;background-color:#faa&#34;&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;&lt;span style=&#34;color:#a00;background-color:#faa&#34;&gt;&lt;/span&gt;      &lt;span style=&#34;color:#09f;font-style:italic&#34;&gt;# Here we add a label to tell Traefik how to route to this service by domain&lt;/span&gt;&lt;span style=&#34;color:#a00;background-color:#faa&#34;&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;&lt;span style=&#34;color:#a00;background-color:#faa&#34;&gt;&lt;/span&gt;      &lt;span style=&#34;color:#09f;font-style:italic&#34;&gt;# Remember to change this host value.&lt;/span&gt;&lt;span style=&#34;color:#a00;background-color:#faa&#34;&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;&lt;span style=&#34;color:#a00;background-color:#faa&#34;&gt;&lt;/span&gt;      - traefik.http.routers.nextcloud.rule&lt;span style=&#34;color:#555&#34;&gt;=&lt;/span&gt;Host&lt;span style=&#34;color:#555&#34;&gt;(&lt;/span&gt;&lt;span style=&#34;color:#c30&#34;&gt;`&lt;/span&gt;nextcloud.example.com&lt;span style=&#34;color:#c30&#34;&gt;`&lt;/span&gt;&lt;span style=&#34;color:#555&#34;&gt;)&lt;/span&gt;&lt;span style=&#34;color:#a00;background-color:#faa&#34;&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;&lt;span style=&#34;color:#a00;background-color:#faa&#34;&gt;&lt;/span&gt;      &lt;span style=&#34;color:#09f;font-style:italic&#34;&gt;# These two labels tell Traefik to setup TLS for this service&lt;/span&gt;&lt;span style=&#34;color:#a00;background-color:#faa&#34;&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;&lt;span style=&#34;color:#a00;background-color:#faa&#34;&gt;&lt;/span&gt;      - traefik.http.routers.nextcloud.tls&lt;span style=&#34;color:#555&#34;&gt;=&lt;/span&gt;true&lt;span style=&#34;color:#a00;background-color:#faa&#34;&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;&lt;span style=&#34;color:#a00;background-color:#faa&#34;&gt;&lt;/span&gt;      - traefik.http.routers.nextcloud.tls.certresolver&lt;span style=&#34;color:#555&#34;&gt;=&lt;/span&gt;myresolver&lt;span style=&#34;color:#a00;background-color:#faa&#34;&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;The above may seem quite verbose, but this is literally &lt;em&gt;all&lt;/em&gt; you need. After running &lt;code&gt;docker-compose up -d&lt;/code&gt; (and you’ve changed your domains and passwords, etc.) your services will be up and running with TLS certificates automatically provisioned.&lt;/p&gt;
&lt;p&gt;I’ve added comments to the file to explain some of the concepts further, but below I’ll add a few extra notes:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;I’ve setup four services: the Traefik reverse proxy itself, a &lt;code&gt;freshrss&lt;/code&gt; container, and two containers for Nextcloud: &lt;code&gt;nextcloud&lt;/code&gt; (the software itself), and &lt;code&gt;nextclouddb&lt;/code&gt; (the MariaDB database for Nextcloud).&lt;/li&gt;
&lt;li&gt;Since &lt;code&gt;nextclouddb&lt;/code&gt; doesn’t need to (and shouldn’t, for security) be exposed to the internet, we do not add any Traefik routing labels to this service.&lt;/li&gt;
&lt;li&gt;Adding a mount for the &lt;code&gt;acme.json&lt;/code&gt; storage is important in your Traefik setup to avoid hitting Let’s Encrypt rate limits.&lt;/li&gt;
&lt;li&gt;I added volumes for FreshRSS and Nextcloud so that data can be persisted.&lt;/li&gt;
&lt;li&gt;Notice how all Traefik routing and TLS configuration on services is simply handled with labels.&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;Whilst the above setup achieves what we need, it is simple. There is much more you can do, such as route using specific paths and manage load balancing. For more information, please see &lt;a href=&#34;https://doc.traefik.io/traefik/routing/overview&#34; target=&#34;_blank&#34; rel=&#34;noopener noreferrer&#34; &gt;the documentation&lt;/a&gt;.&lt;/p&gt;
&lt;h2 id=&#34;more-complex-services&#34;&gt;More complex services&lt;/h2&gt;
&lt;p&gt;As mentioned earlier, I also run Gitea as a service on my VPS. Gitea serves a GitHub-style web UI in addition to a git endpoint. Since I interact with the git endpoints over SSH, this service ends up relying on two ingress points: one for HTTP web traffic and another for SSH git traffic.&lt;/p&gt;
&lt;p&gt;When I first set this up, Traefik was routing web traffic through to Gitea’s SSH port, which obviously caused problems. As such, I needed to add extra configuration to tell Traefik which port to use, as described below:&lt;/p&gt;
&lt;div class=&#34;highlight&#34;&gt;&lt;pre tabindex=&#34;0&#34; style=&#34;background-color:#f0f3f3;-moz-tab-size:2;-o-tab-size:2;tab-size:2;&#34;&gt;&lt;code class=&#34;language-docker&#34; data-lang=&#34;docker&#34;&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;...&lt;span style=&#34;color:#a00;background-color:#faa&#34;&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;&lt;span style=&#34;color:#a00;background-color:#faa&#34;&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;&lt;span style=&#34;color:#a00;background-color:#faa&#34;&gt;&lt;/span&gt;  gitea:&lt;span style=&#34;color:#a00;background-color:#faa&#34;&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;&lt;span style=&#34;color:#a00;background-color:#faa&#34;&gt;&lt;/span&gt;    image: gitea/gitea:latest&lt;span style=&#34;color:#a00;background-color:#faa&#34;&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;&lt;span style=&#34;color:#a00;background-color:#faa&#34;&gt;&lt;/span&gt;    restart: always&lt;span style=&#34;color:#a00;background-color:#faa&#34;&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;&lt;span style=&#34;color:#a00;background-color:#faa&#34;&gt;&lt;/span&gt;    environment:&lt;span style=&#34;color:#a00;background-color:#faa&#34;&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;&lt;span style=&#34;color:#a00;background-color:#faa&#34;&gt;&lt;/span&gt;      - &lt;span style=&#34;color:#033&#34;&gt;USER_UID&lt;/span&gt;&lt;span style=&#34;color:#555&#34;&gt;=&lt;/span&gt;&lt;span style=&#34;color:#f60&#34;&gt;1000&lt;/span&gt;&lt;span style=&#34;color:#a00;background-color:#faa&#34;&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;&lt;span style=&#34;color:#a00;background-color:#faa&#34;&gt;&lt;/span&gt;      - &lt;span style=&#34;color:#033&#34;&gt;USER_GID&lt;/span&gt;&lt;span style=&#34;color:#555&#34;&gt;=&lt;/span&gt;&lt;span style=&#34;color:#f60&#34;&gt;1000&lt;/span&gt;&lt;span style=&#34;color:#a00;background-color:#faa&#34;&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;&lt;span style=&#34;color:#a00;background-color:#faa&#34;&gt;&lt;/span&gt;    volumes:&lt;span style=&#34;color:#a00;background-color:#faa&#34;&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;&lt;span style=&#34;color:#a00;background-color:#faa&#34;&gt;&lt;/span&gt;      - ./gitea_data:/data&lt;span style=&#34;color:#a00;background-color:#faa&#34;&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;&lt;span style=&#34;color:#a00;background-color:#faa&#34;&gt;&lt;/span&gt;      - /etc/timezone:/etc/timezone:ro&lt;span style=&#34;color:#a00;background-color:#faa&#34;&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;&lt;span style=&#34;color:#a00;background-color:#faa&#34;&gt;&lt;/span&gt;      - /etc/localtime:/etc/localtime:ro&lt;span style=&#34;color:#a00;background-color:#faa&#34;&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;&lt;span style=&#34;color:#a00;background-color:#faa&#34;&gt;&lt;/span&gt;    expose:&lt;span style=&#34;color:#a00;background-color:#faa&#34;&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;&lt;span style=&#34;color:#a00;background-color:#faa&#34;&gt;&lt;/span&gt;      - &lt;span style=&#34;color:#c30&#34;&gt;&#34;3000&#34;&lt;/span&gt;&lt;span style=&#34;color:#a00;background-color:#faa&#34;&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;&lt;span style=&#34;color:#a00;background-color:#faa&#34;&gt;&lt;/span&gt;    ports:&lt;span style=&#34;color:#a00;background-color:#faa&#34;&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;&lt;span style=&#34;color:#a00;background-color:#faa&#34;&gt;&lt;/span&gt;      &lt;span style=&#34;color:#09f;font-style:italic&#34;&gt;# Ensure we map port 22 to the host for incoming SSH git traffic&lt;/span&gt;&lt;span style=&#34;color:#a00;background-color:#faa&#34;&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;&lt;span style=&#34;color:#a00;background-color:#faa&#34;&gt;&lt;/span&gt;      - &lt;span style=&#34;color:#c30&#34;&gt;&#34;22:22&#34;&lt;/span&gt;&lt;span style=&#34;color:#a00;background-color:#faa&#34;&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;&lt;span style=&#34;color:#a00;background-color:#faa&#34;&gt;&lt;/span&gt;    labels:&lt;span style=&#34;color:#a00;background-color:#faa&#34;&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;&lt;span style=&#34;color:#a00;background-color:#faa&#34;&gt;&lt;/span&gt;      &lt;span style=&#34;color:#09f;font-style:italic&#34;&gt;# The below three labels are as described earlier&lt;/span&gt;&lt;span style=&#34;color:#a00;background-color:#faa&#34;&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;&lt;span style=&#34;color:#a00;background-color:#faa&#34;&gt;&lt;/span&gt;      - traefik.http.routers.gitea.rule&lt;span style=&#34;color:#555&#34;&gt;=&lt;/span&gt;Host&lt;span style=&#34;color:#555&#34;&gt;(&lt;/span&gt;&lt;span style=&#34;color:#c30&#34;&gt;`&lt;/span&gt;git.example.com&lt;span style=&#34;color:#c30&#34;&gt;`&lt;/span&gt;&lt;span style=&#34;color:#555&#34;&gt;)&lt;/span&gt;&lt;span style=&#34;color:#a00;background-color:#faa&#34;&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;&lt;span style=&#34;color:#a00;background-color:#faa&#34;&gt;&lt;/span&gt;      - traefik.http.routers.gitea.tls&lt;span style=&#34;color:#555&#34;&gt;=&lt;/span&gt;true&lt;span style=&#34;color:#a00;background-color:#faa&#34;&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;&lt;span style=&#34;color:#a00;background-color:#faa&#34;&gt;&lt;/span&gt;      - traefik.http.routers.gitea.tls.certresolver&lt;span style=&#34;color:#555&#34;&gt;=&lt;/span&gt;myresolver&lt;span style=&#34;color:#a00;background-color:#faa&#34;&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;&lt;span style=&#34;color:#a00;background-color:#faa&#34;&gt;&lt;/span&gt;      &lt;span style=&#34;color:#09f;font-style:italic&#34;&gt;# Add a fourth label to tell Traefik where to route web traffic to&lt;/span&gt;&lt;span style=&#34;color:#a00;background-color:#faa&#34;&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;&lt;span style=&#34;color:#a00;background-color:#faa&#34;&gt;&lt;/span&gt;      - traefik.http.services.gitea.loadbalancer.server.port&lt;span style=&#34;color:#555&#34;&gt;=&lt;/span&gt;&lt;span style=&#34;color:#f60&#34;&gt;3000&lt;/span&gt;&lt;span style=&#34;color:#a00;background-color:#faa&#34;&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;After running &lt;code&gt;docker-compose up -d&lt;/code&gt; again, your Gitea instance will be running, along with its web UI and git endpoint.&lt;/p&gt;
&lt;p&gt;&lt;em&gt;Note: if you use port 22 for standard SSH to your host server, you can map a different port for your SSH git traffic. For example, &lt;code&gt;2200:22&lt;/code&gt;.&lt;/em&gt;&lt;/p&gt;
&lt;h2 id=&#34;conclusion&#34;&gt;Conclusion&lt;/h2&gt;
&lt;p&gt;Having made this switch, I find things much more logically organised and robust. The reverse proxy is just as performant (at least, for my use) as Nginx and I get extra peace of mind in that I can trust Traefik to handle web traffic, routing, and certificate renewals without any manual intervention.&lt;/p&gt;
</content:encoded>
    </item>
    
    <item>
      <title>Interacting with a Nextcloud instance deployed with Docker</title>
      <link>https://wilw.dev/blog/2022/01/28/nextcloud-docker-control/</link>
      <pubDate>Fri, 28 Jan 2022 19:18:00 +0000</pubDate>
      
      <guid>https://wilw.dev/blog/2022/01/28/nextcloud-docker-control/</guid>
      
        <category>technology</category>
      
        <category>selfhost</category>
      
      
      <content:encoded>&lt;p&gt;If you’ve ever run your own &lt;a href=&#34;https://nextcloud.com&#34; target=&#34;_blank&#34; rel=&#34;noopener noreferrer&#34; &gt;Nextcloud&lt;/a&gt; before, you may have noticed screens like the following in your instance’s settings pages.&lt;/p&gt;
&lt;p&gt;&lt;img src=&#34;https://wilw.dev/media/blog/nextcloud_occ_1.png&#34; alt=&#34;Nextcloud administration interface showing outstanding maintenance tasks&#34;&gt;&lt;/p&gt;
&lt;p&gt;The messages advise a number of maintenance procedures to help ensure the smooth running of your instance. These could be to run database migrations or to update schemas in response to installing new apps.&lt;/p&gt;
&lt;p&gt;Often these steps might involve running &lt;code&gt;occ&lt;/code&gt; commands. &lt;a href=&#34;https://docs.nextcloud.com/server/latest/admin_manual/configuration_server/occ_command.html&#34; target=&#34;_blank&#34; rel=&#34;noopener noreferrer&#34; &gt;&lt;code&gt;occ&lt;/code&gt;&lt;/a&gt; is Nextcloud’s command-line interface, and is so-called because of its origins in ownCloud.&lt;/p&gt;
&lt;p&gt;If you deploy your Nextcloud using Docker, then it isn’t immediately obvious how to begin invoking the &lt;code&gt;occ&lt;/code&gt; command.&lt;/p&gt;
&lt;h2 id=&#34;invoking-occ-on-a-running-nextcloud-docker-container&#34;&gt;Invoking &lt;code&gt;occ&lt;/code&gt; on a running Nextcloud Docker container&lt;/h2&gt;
&lt;p&gt;Luckily, Docker makes this straight forward if you run Nextcloud in a container.&lt;/p&gt;
&lt;p&gt;Assuming you manage your Docker orchestration using &lt;a href=&#34;https://docs.docker.com/compose&#34; target=&#34;_blank&#34; rel=&#34;noopener noreferrer&#34; &gt;Docker Compose&lt;/a&gt;, if you wanted to run the &lt;code&gt;occ db:add-missing-indices&lt;/code&gt; command from the screenshot above on a Nextcloud Docker container, you could run the following from your project’s directory:&lt;/p&gt;
&lt;pre tabindex=&#34;0&#34;&gt;&lt;code&gt;$ docker-compose exec --user www-data nextcloud php occ db:add-missing-indices
&lt;/code&gt;&lt;/pre&gt;&lt;p&gt;Where &lt;code&gt;nextcloud&lt;/code&gt; is the service’s name in your &lt;code&gt;docker-compose.yml&lt;/code&gt; file&lt;/p&gt;
&lt;p&gt;If you don’t use Docker Compose, the same can be achieved using &lt;code&gt;docker exec&lt;/code&gt; directly:&lt;/p&gt;
&lt;pre tabindex=&#34;0&#34;&gt;&lt;code&gt;$ docker exec --user www-data 0e4c3hd9s049 php occ db:add-missing-indices
&lt;/code&gt;&lt;/pre&gt;&lt;p&gt;Where &lt;code&gt;0e4c3hd9s049&lt;/code&gt; is the ID for your Nextcloud container, which you can find by running &lt;code&gt;docker ps&lt;/code&gt; first.&lt;/p&gt;
&lt;p&gt;&lt;img src=&#34;https://wilw.dev/media/blog/nextcloud_occ_2.png&#34; alt=&#34;Result of running occ via docker-compose exec&#34;&gt;&lt;/p&gt;
&lt;p&gt;In both cases, the command is structured the same. You may wish to note the &lt;code&gt;--user&lt;/code&gt; flag. In my case, I tell Docker to run the command as &lt;code&gt;www-data&lt;/code&gt; as that is the user that owns the Nextcloud &lt;code&gt;config/config.php&lt;/code&gt; file in my setup. If you use a different user, then update this value in your command.&lt;/p&gt;
&lt;p&gt;You may also need to run the &lt;code&gt;docker-compose&lt;/code&gt; and &lt;code&gt;docker&lt;/code&gt; commands as a superuser, depending on whether your normal user is in the appropriate group.&lt;/p&gt;
</content:encoded>
    </item>
    
    <item>
      <title>The future of the decentralised web: thoughts on web0 and web3</title>
      <link>https://wilw.dev/blog/2022/01/21/web0-web3/</link>
      <pubDate>Fri, 21 Jan 2022 08:27:00 +0000</pubDate>
      
      <guid>https://wilw.dev/blog/2022/01/21/web0-web3/</guid>
      
        <category>technology</category>
      
        <category>opinion</category>
      
      
      <content:encoded>&lt;p&gt;I recently signed the &lt;a href=&#34;https://web0.small-web.org&#34; target=&#34;_blank&#34; rel=&#34;noopener noreferrer&#34; &gt;web0 manifesto&lt;/a&gt;, which embodies many of the values I consider to be important when it comes to technology - and the web in particular.&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;web0 is the decentralised web… web0 is web3 without all the corporate right-libertarian Silicon Valley bullshit.&lt;/p&gt;
&lt;p&gt;– &lt;a href=&#34;https://web0.small-web.org&#34; target=&#34;_blank&#34; rel=&#34;noopener noreferrer&#34; &gt;The web0 manifesto website&lt;/a&gt;&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;Essentially web0 is around empowering a decentralised web that:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;Is available and truly accessible to everyone;&lt;/li&gt;
&lt;li&gt;Minimises the barriers to entry for all;&lt;/li&gt;
&lt;li&gt;Enables people to own and control their own data (sovereignty);&lt;/li&gt;
&lt;li&gt;Enables people to move, remove, or modify their own data;&lt;/li&gt;
&lt;li&gt;Avoids the naturally-occurring centralisation when utilising big-tech services;&lt;/li&gt;
&lt;li&gt;… And therefore puts the web back in the control of individuals rather than a small number of huge companies.&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;In practice this could mean owning your own domain name and taking part by hosting a website or through getting involved in other communities, such as in the tildeverse. The key thing is that participants own and can control their own data and that things are accomplished without needing to rely on big-tech.&lt;/p&gt;
&lt;p&gt;Otherwise, there are no clearly defined objectives specified in the manifesto, which in itself demonstrates the freedom in the values it is trying to promote. Furthermore, web0 is not in itself aiming to become a trend or a recognised movement - it is intended more as a guiding set of principles that rejects the use of some technologies in order to achieve its goals.&lt;/p&gt;
&lt;h2 id=&#34;decentralisation-and-web3&#34;&gt;Decentralisation and web3&lt;/h2&gt;
&lt;p&gt;The fundamental principles behind web3 share some similarities with those exhibited by the ideas of web0. Notions such as removing the reliance on “big tech”, making tools accessible to all, and helping to facilitate the return of data ownership and control back to the individuals involved are certainly shared amongst the two movements.&lt;/p&gt;
&lt;p&gt;And in terms of the underlying technology itself relevant to web3 - i.e. permissionless distributed blockchain and smart contracts - this is essentially achieved, too. The technologies are already well-proven in a number of fields - from finance and insurance through to legal and supply-chain, but their implementation as a core web technology still faces a number of issues. This “web3” aims to replace traditional web components (servers, databases, accounts, etc.) with blockchain-native concepts&lt;/p&gt;
&lt;p&gt;The issues - rather than the technologies themselves (though these can be problematic too) - are part of the motivations for publishing the web0 manifesto, and are largely related to the &lt;em&gt;way&lt;/em&gt; these tools are being leveraged, and the creep of financially powerful individuals and organisations into the space.&lt;/p&gt;
&lt;p&gt;With early adopters and VCs understanding that by growing enthusiasm and intrigue in the world of web3 and crypto they can hugely maximise their return on investments, self-worth, and perceived positions in the crypto echelons, they naturally want to drive additional growth in mid-to-late adopters. It is a consequence that those towards the end of the “hype train” lose out by engaging at a time when the decentralised digital assets (and the transactions that power their creation and movement) have gained massively inflated prices.&lt;/p&gt;
&lt;p&gt;Some still choose to engage at this time as they are convinced by the excitement (marketing?) and assume they will also be able to join in on the riches and reward so publicly flaunted by their peers and celebrities alike.&lt;/p&gt;
&lt;p&gt;Trends such as including ENS (&lt;code&gt;.eth&lt;/code&gt;) names in Twitter profiles and the use of NFT artwork as avatars and imagery across social platforms only serve to develop the hype. Some of these communities (“crypto bros”) even use toxicity to further their cause and will aggressively rebuke any motions made against crypto ethics - no matter how well founded the argument.&lt;/p&gt;
&lt;p&gt;The upshot is that now most people are completely priced out of getting involved.  &lt;code&gt;.eth&lt;/code&gt; domains are a nice way of providing a basic public identity to your web3 interactions; compared to traditional DNS names they’re a bargain at $5/year, but given the gas fees (at the time of writing) mean you need to fork out hundreds of dollars worth of ETH just to complete the transactions required in order to own one, this doesn’t scream openness and accessibility.&lt;/p&gt;
&lt;p&gt;NFT marketplaces have been hailed as a way to revolutionise art and to put the power in the hands of the creators. If this is the case, then that’s fantastic, but I worry about up-and-coming artists losing out financially when they realise that the fees they need to pay in order to transfer the work after a purchase may far exceed the sale price itself.&lt;/p&gt;
&lt;p&gt;Many of these issues are related to the limitations of the blockchain technology currently most relevant to web3 - &lt;a href=&#34;https://ethereum.org&#34; target=&#34;_blank&#34; rel=&#34;noopener noreferrer&#34; &gt;Ethereum&lt;/a&gt;. Various technical factors result in a limited transaction throughput, and with increased hype in the space, the demand (and thus cost) for computation on the network also increases. Of course, other technologies exist that try to solve these problems; &lt;a href=&#34;https://solana.com&#34; target=&#34;_blank&#34; rel=&#34;noopener noreferrer&#34; &gt;Solana&lt;/a&gt; has lots of promise but still has some way to go in competing with Ethereum in terms of popularity. There have also been a few questions raised about &lt;a href=&#34;https://www.businessinsider.in/cryptocurrency/news/ethereum-killer-solana-network-crashes-for-third-time-in-six-months/articleshow/88703869.cms&#34; target=&#34;_blank&#34; rel=&#34;noopener noreferrer&#34; &gt;Solana’s stability&lt;/a&gt;.&lt;/p&gt;
&lt;p&gt;Of course, things may change a little when &lt;a href=&#34;https://ethereum.org/en/eth2/merge/&#34; target=&#34;_blank&#34; rel=&#34;noopener noreferrer&#34; &gt;Ethereum’s proof-of-stake merge&lt;/a&gt; completes later this year, anyway. People with less disposable funds may then be able to participate, but asset prices and NFTs are unlikely to decrease because of this. Either way, the technical know-how required to set-up wallets and trade crypto assets presents intrinsic friction which, when combined with the fact that there’s essentially zero protection if passphrases or private keys are lost, presents alien concepts to new users.&lt;/p&gt;
&lt;p&gt;web3 and web0 might both purport to be focussed on decentralisation and accessibility, but it is clear that there is a difference in the type of people that have the resources and capability to actively get involved in each.&lt;/p&gt;
&lt;h2 id=&#34;how-decentralised-is-web3-really&#34;&gt;How decentralised is web3, really?&lt;/h2&gt;
&lt;p&gt;Moxie Marlinspike‘s &lt;a href=&#34;https://moxie.org/2022/01/07/web3-first-impressions.html&#34; target=&#34;_blank&#34; rel=&#34;noopener noreferrer&#34; &gt;recent and excellent post on web3&lt;/a&gt; highlighted several issues that hint at a couple of growing trends that essentially centralise web3 principles.&lt;/p&gt;
&lt;p&gt;The first is that nearly all crypto wallets and dApps (decentralised apps, in which the blockchain is the database) interface with the blockchain through a handful of well-known providers, such as &lt;a href=&#34;https://infura.io&#34; target=&#34;_blank&#34; rel=&#34;noopener noreferrer&#34; &gt;Infura&lt;/a&gt; or &lt;a href=&#34;https://www.alchemy.com&#34; target=&#34;_blank&#34; rel=&#34;noopener noreferrer&#34; &gt;Alchemy&lt;/a&gt;.&lt;/p&gt;
&lt;p&gt;The second is that these wallets also use the &lt;a href=&#34;https://docs.opensea.io/reference/api-overview&#34; target=&#34;_blank&#34; rel=&#34;noopener noreferrer&#34; &gt;Opensea API&lt;/a&gt; for retrieving and displaying NFTs. This means that if Opensea chooses to delist your NFTs for any reason (as they did in Moxie’s post), then suddenly they cease to exist in wallets too - even if they continue to live as tokens on the chain itself. Like any other company, Opensea can make its own decisions about how it acts, and is also subject to external pressures (government, police and financial included) to act in different ways, which could have dramatic effects on the NFT economy.&lt;/p&gt;
&lt;p&gt;As before, this has a greater impact on the creative industry, where the visual aspect of the tokens (and thus where and the way they are rendered) is more important than the technical effects of having the token in one’s wallet.&lt;/p&gt;
&lt;p&gt;Both of these factors are antithesis to what web3 is supposed to - and what web0 certainly does - stand for. Suddenly people are back in the position of losing their data sovereignty to a small number of tech companies that are gathering increasingly more control over the ecosystem. If things continue down this route, as it surely will with more and more large companies becoming involved in the space - then it won’t be long until the blockchain component is completely forgotten and everything boils down to centralise around a handful of platforms running the whole show. Sound familiar?&lt;/p&gt;
&lt;p&gt;&lt;a href=&#34;https://unstoppabledomains.com&#34; target=&#34;_blank&#34; rel=&#34;noopener noreferrer&#34; &gt;Unstoppable Domains&lt;/a&gt; is another example of a &lt;a href=&#34;https://www.crunchbase.com/organization/unstoppable-domains/company_financials&#34; target=&#34;_blank&#34; rel=&#34;noopener noreferrer&#34; &gt;VC-invested company&lt;/a&gt; getting in on the web3 action. They let you buy domains that sit as NFTs on the Polygon protocol (an Ethereum layer 2 solution). Whilst I certainly agree that domain squatting is bad practice, preventing people from purchasing domains because they happen to have a name similar to a “notable individual” is also an example of corporate control over the web3 ecosystem - no matter what the intention is.&lt;/p&gt;
&lt;p&gt;&lt;img src=&#34;https://wilw.dev/media/blog/unstoppable_domains.png&#34; alt=&#34;Unstoppable Domains domain-search, showing wilw.nft as &amp;ldquo;protected&amp;rdquo;&#34;&gt;&lt;/p&gt;
&lt;p&gt;I totally get that traditional DNS names can also be seized and controlled, but web3 companies should be pushing past these restrictions.&lt;/p&gt;
&lt;p&gt;Apparently their support team has also &lt;a href=&#34;https://www.reddit.com/r/ENSMarket/comments/qd0jkl/ens_vs_unstoppable&#34; target=&#34;_blank&#34; rel=&#34;noopener noreferrer&#34; &gt;admitted&lt;/a&gt; that their goal is “adoption” (i.e. higher sales) rather than “permissionless” (i.e. fully decentralised).&lt;/p&gt;
&lt;p&gt;Whilst, of course, we shouldn’t necessarily be &lt;em&gt;against&lt;/em&gt; companies trying to advocate for decentralisation, I think they just need to be wary about undermining the very world they are trying to build. Their priority is always their shareholders; not the general good.&lt;/p&gt;
&lt;h2 id=&#34;some-closing-thoughts&#34;&gt;Some closing thoughts&lt;/h2&gt;
&lt;p&gt;Whilst I back decentralisation and the accessibility and availability-to-all ideologies of web0 wholeheartedly, I appreciate that there could eventually be avenues for collaboration between the underlying concepts from both movements.&lt;/p&gt;
&lt;p&gt;Some people say that web3 is still in its infancy. It’s hard to say how true this is when the technology itself has been in mainstream use for over a decade, but if there is hope for a dramatic reduction in the barrier-to-entry for this suite of technologies, and if the ethical and human issues can be resolved, then I can see some potential for a decentralised transfer of tokens and uniqueness of assets being useful in a web context.&lt;/p&gt;
&lt;p&gt;For example, if a truly decentralised and permissionless asset system to represent user accounts can involve tokens being transferred and used anonymously between &lt;em&gt;people&lt;/em&gt;-oriented services, and which doesn’t in itself exclude people based on position or financial capability, then I think that would be a positive outcome.&lt;/p&gt;
&lt;p&gt;Either way, blockchain tech, digital assets, and the entire ecosystem is certainly not going anywhere, and the uses for these tools are expanding every day. Though until the issues around adaptation to the web and mainstream accessibility can be resolved then they may not yet be ready to form the foundations for a web that is truly available to all.&lt;/p&gt;
&lt;p&gt;web0, on the other hand, is perfectly positioned as a concept powering an ecosystem that anyone and everyone can participate in. We’ve already seen a significant recent growth in the people and organisations adopting &lt;a href=&#34;https://ar.al/2020/08/07/what-is-the-small-web/&#34; target=&#34;_blank&#34; rel=&#34;noopener noreferrer&#34; &gt;small web principles&lt;/a&gt; and I am excited to see how this can help form a healthy web for the future.&lt;/p&gt;
</content:encoded>
    </item>
    
    <item>
      <title>Completing the #100DaysToOffload challenge</title>
      <link>https://wilw.dev/blog/2022/01/13/100-days-in-review/</link>
      <pubDate>Thu, 13 Jan 2022 19:19:00 +0000</pubDate>
      
      <guid>https://wilw.dev/blog/2022/01/13/100-days-in-review/</guid>
      
        <category>100daystooffload</category>
      
        <category>life</category>
      
      
      <content:encoded>&lt;p&gt;Twelve months ago - in January 2021 - I started my attempt at the #100DaysToOffload challenge. I had set myself a new year’s resolution to try to write more and, around the same time, I noticed the hashtag for the challenge circulating on Mastodon. It seemed like the perfect opportunity to fulfil my resolution. The challenge is to post 100 times on a personal blog during the space of one year.&lt;/p&gt;
&lt;p&gt;Whilst I had a blog (on this website) before starting the challenge, I only really wrote on it sporadically. In this post I’ll talk a little about how I approached the challenge, and about how I came to be able to post more frequently during the year (and hopefully beyond!).&lt;/p&gt;
&lt;h2 id=&#34;interacting-with-other-challengers&#34;&gt;Interacting with other challengers&lt;/h2&gt;
&lt;p&gt;I began by visiting the &lt;a href=&#34;https://100daystooffload.com&#34; target=&#34;_blank&#34; rel=&#34;noopener noreferrer&#34; &gt;challenge website&lt;/a&gt; to follow the feeds of other bloggers that had previously completed the challenge. I also searched the hashtag in the fediverse to discover people currently attempting it.&lt;/p&gt;
&lt;p&gt;My aim was to follow and interact with others in order to gain inspiration and ideas for posts. However, I quickly realised that people wrote about things that meant something to &lt;em&gt;them&lt;/em&gt; - a personal blog is exactly that: a series of posts about things the author is interested in or wants to share. This is the entire point of the challenge in the first place; it has to be completed on a personal blog.&lt;/p&gt;
&lt;p&gt;Despite this, following other frequent bloggers certainly provided encouragement and motivation to get started and to keep going, which was in itself invaluable.&lt;/p&gt;
&lt;h2 id=&#34;planning-when-to-write&#34;&gt;Planning “when” to write&lt;/h2&gt;
&lt;p&gt;As someone who typically blogged only a handful of times a year previously, I needed to plan an approach that would enable me to write more frequently.&lt;/p&gt;
&lt;p&gt;To get 100 posts published in 2021, I’d need to write about two per week. I set myself a “Write blog post!” Reminder and configured it to repeat twice a week: on Wednesdays and Saturdays. I use the &lt;a href=&#34;https://www.sortedapp.com&#34; target=&#34;_blank&#34; rel=&#34;noopener noreferrer&#34; &gt;Sorted&lt;/a&gt; app, which made this easy, but any software that supports repeated “reminders” would work.&lt;/p&gt;
&lt;p&gt;This way, even if I was unable to write my post on a particular Wednesday in which I was particularly busy, the reminder would still show as “todo” on the Thursday. This helped me to stay on track.&lt;/p&gt;
&lt;h2 id=&#34;planning-what-to-write&#34;&gt;Planning “what” to write&lt;/h2&gt;
&lt;p&gt;Planning &lt;em&gt;when&lt;/em&gt; to write is the easy part. The problem is then working out &lt;em&gt;what&lt;/em&gt; to write about.&lt;/p&gt;
&lt;p&gt;I always keep a “scratchpad” note on my notes app in which I keep random links, bookmarks, and other things to eventually think about, visit, or to filter through to a better record-keeping system.&lt;/p&gt;
&lt;p&gt;I made a section at the top of this note called “Blog post ideas” and began to populate it with a handful of short bullet points for potential topics. I added sub-points to each topic in order to add extra context or ideas related to that topic that I could also include.&lt;/p&gt;
&lt;p&gt;I use the &lt;a href=&#34;https://bear.app&#34; target=&#34;_blank&#34; rel=&#34;noopener noreferrer&#34; &gt;Bear&lt;/a&gt; app, which meant I could easily note down ideas as they came to me when out and about, but any app that syncs well between devices would do the job.&lt;/p&gt;
&lt;p&gt;To start with, the list contained some software dependency packages I’d recently discovered and found interesting. My intention was to write short posts to explain how to use the packages, describe their purpose, and some thoughts about why I had decided to use them.&lt;/p&gt;
&lt;p&gt;I then began to think of my own life, and what was potentially noteworthy from that angle. I began to bullet about some interesting books I’d recently read, some podcasts I subscribe to, and the fact I had recently got a dog. I had also recently started exercising more, working from home, and had ideas about achieving balance in those areas.&lt;/p&gt;
&lt;p&gt;Before too long, I had a bank of around 20 post ideas in my scratchpad note. This made the task seem much less daunting!&lt;/p&gt;
&lt;p&gt;The ideas themselves weren’t very exciting, but I had angles I could use to make them interesting or useful to others whilst keeping them personal.&lt;/p&gt;
&lt;h2 id=&#34;getting-over-the-inertia-of-writing--some-learnings&#34;&gt;Getting over the inertia of writing &amp; some learnings&lt;/h2&gt;
&lt;p&gt;I’m not a natural writer. One of the reasons I blogged so infrequently before 2021 was a fear and apprehension about others reading - and judging - what I write. This made it hard to initially put “pen to paper” and get started.&lt;/p&gt;
&lt;p&gt;This fear also made the process &lt;em&gt;long&lt;/em&gt;. Short posts that should take 20 minutes to draft, check, and publish would often take me hours. I’d proof-read again and again and try to take on additional viewpoints. This made the act of then physically posting the article even more difficult, and even after posting it I’d re-read it and continue to make post-publish edits.&lt;/p&gt;
&lt;p&gt;The volume and frequency of writing required of the challenge meant I didn’t have time for all of this anymore. I still wanted a life and the ability to work on other things in my spare time (where it exists!), and so spending several hours twice a week on blog-writing just wouldn’t fit.&lt;/p&gt;
&lt;p&gt;As such, I quickly learned to &lt;em&gt;just start writing&lt;/em&gt;. After I was a few posts into the challenge I began to treat each one as if it were a more casual email or as if I were simply drafting a toot (or a tweet).&lt;/p&gt;
&lt;p&gt;It was a new mental model that allowed me to write without fear, and I quickly began to look forward to Wednesdays and Saturdays and to getting down to writing and publishing a new post.&lt;/p&gt;
&lt;p&gt;Before I knew it I was 10 days, then 20, and then 30 days into the challenge. I’d continue to add new ideas to the scratchpad to keep up the backlog, and the whole process began to take on a rhythm of its own.&lt;/p&gt;
&lt;h2 id=&#34;takeaways&#34;&gt;Takeaways&lt;/h2&gt;
&lt;p&gt;I now feel much more confident in my writing. I know that not everyone will find my posts useful or interesting (far from it), but I’ve begun to enjoy writing simply for the sake of it. I also get a few comments from others who read and subscribe to my blog, which is nice.&lt;/p&gt;
&lt;p&gt;I still maintain my “Blog post ideas” list, and continue to add to it. I hope to carry on working through these in 2022. I found I’ve re-learned my ability to explore the web and all it has to offer. I take the time to read about articles, posts, and new developments, and to form opinions and thoughts that - in turn - spark the ideas for new thought pieces of my own.&lt;/p&gt;
&lt;p&gt;I’ve also loved reading and engaging with the posts published by others taking part in the challenge. It’s a great mix of personal content, technical reviews and tutorials, and lots more. My RSS feed is always full of interesting posts (which reminds me that I need to create a blog roll for my website soon…), and I feel that I learn a huge amount from getting involved.&lt;/p&gt;
&lt;p&gt;Writing my blog also encouraged me to spend more time on my personal website. I wanted to make it more accessible, readable and engaging, whilst keeping things as simple as possible. I added new pages (such as a “notes” page), and tried to link to other projects and inspirations where possible.&lt;/p&gt;
&lt;p&gt;As a result of the blog and the changes to my site I noticed a 20-30x increase in web traffic. Some posts got popular enough for them to get shared and boosted more widely, or even cross-shared by others to other websites and I’d receive 250x my usual pre-challenge daily traffic.&lt;/p&gt;
&lt;p&gt;Whilst I won’t immediately take part in the challenge again at the start of 2022, I hope to certainly do so again in the future. Either way, I will be writing more frequently and with improved confidence.&lt;/p&gt;
&lt;p&gt;The #100DaysToOffload challenge is certainly something that I can recommend to anyone who has a little time to spare a couple of times a week. Thank you to &lt;a href=&#34;https://kevq.uk&#34; target=&#34;_blank&#34; rel=&#34;noopener noreferrer&#34; &gt;Kev&lt;/a&gt; for setting it up and for being so encouraging throughout!&lt;/p&gt;
</content:encoded>
    </item>
    
    <item>
      <title>Browser push notifications in a Flutter web application</title>
      <link>https://wilw.dev/blog/2021/12/18/flutter-web-push/</link>
      <pubDate>Sat, 18 Dec 2021 10:03:00 +0000</pubDate>
      
      <guid>https://wilw.dev/blog/2021/12/18/flutter-web-push/</guid>
      
        <category>100daystooffload</category>
      
        <category>technology</category>
      
      
      <content:encoded>&lt;p&gt;&lt;strong&gt;🎉 This is post 100 in my attempt at the #100DaysToOffload challenge!&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;For a couple of years I have been writing mobile apps using the &lt;a href=&#34;https://flutter.dev&#34; target=&#34;_blank&#34; rel=&#34;noopener noreferrer&#34; &gt;Flutter framework&lt;/a&gt;, having previously been a &lt;a href=&#34;https://reactnative.dev&#34; target=&#34;_blank&#34; rel=&#34;noopener noreferrer&#34; &gt;React Native&lt;/a&gt; advocate. Flutter is a great tool for writing applications that target multiple platforms and architectures from one code base - and not needing to write any JavaScript is definitely a bonus!&lt;/p&gt;
&lt;p&gt;I use and recommend &lt;a href=&#34;https://firebase.google.com/docs/cloud-messaging&#34; target=&#34;_blank&#34; rel=&#34;noopener noreferrer&#34; &gt;Firebase Cloud Messaging&lt;/a&gt; to handle push notifications in these applications. There’s also a great library for Flutter - &lt;a href=&#34;https://firebase.flutter.dev/docs/messaging/overview&#34; target=&#34;_blank&#34; rel=&#34;noopener noreferrer&#34; &gt;Flutterfire&lt;/a&gt; - to handle the setup and receipt of these messages, along with the requesting of push permissions on iOS. The set-up takes away the pain of managing cross-platform notifications in Android and iOS applications.&lt;/p&gt;
&lt;p&gt;I recently needed to publish a front-end web version of one of these apps. The back-end system (web API) would remain the same, but I was initially daunted by the task of needing to write a whole new web application mirroring the mobile client functionality.&lt;/p&gt;
&lt;p&gt;However, I then realised I could (pretty much) simply issue a &lt;code&gt;flutter build web&lt;/code&gt; command to get a web-based version of my mobile app. This worked beautifully and it made me remember why I enjoy using this framework so much! As a result of the command, a fully-responsive web app is built and is instantly deployable.&lt;/p&gt;
&lt;p&gt;The only piece that didn’t quite work so well out of the box in the web version was the Firebase push notification system - the webapp would run fine but would crash with errors when trying to request push permission or interact with the Firebase SDK in other ways.&lt;/p&gt;
&lt;p&gt;It seemed that additional setup would be required to allow for web-based push notifications.&lt;/p&gt;
&lt;h2 id=&#34;initializing-a-flutter-app&#34;&gt;Initializing a Flutter app&lt;/h2&gt;
&lt;p&gt;Both the Flutterfire documentation and the Firebase console include guides and documentation on setting-up push notifications in applications, however a little deviation is required for this to work seamlessly for both web and mobile applications from a single Flutter codebase.&lt;/p&gt;
&lt;p&gt;In this post I’ll cover my approach to the additional setup required to getting this to work.&lt;/p&gt;
&lt;p&gt;I’ll assume that you have an existing Flutter application to which you want to add web push functionality. I’ll also assume that you’ve already gone through the initial Firebase configuration and installation (i.e. that you have push working on mobile devices). If you don’t yet, the documentation I linked to above covers this better than I can and so I recommend going through that first!&lt;/p&gt;
&lt;h2 id=&#34;setting-up-push-for-web&#34;&gt;Setting-up push for web&lt;/h2&gt;
&lt;p&gt;First off, we need to enable web-based builds for the existing Flutter app. If you haven’t already got a &lt;code&gt;web/&lt;/code&gt; directory in your project, run the following command in the root of your project:&lt;/p&gt;
&lt;div class=&#34;highlight&#34;&gt;&lt;pre tabindex=&#34;0&#34; style=&#34;background-color:#f0f3f3;-moz-tab-size:2;-o-tab-size:2;tab-size:2;&#34;&gt;&lt;code class=&#34;language-shell&#34; data-lang=&#34;shell&#34;&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;flutter create .
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;You should now have a &lt;code&gt;web/&lt;/code&gt; directory with files such as &lt;code&gt;index.html&lt;/code&gt; and &lt;code&gt;manifest.json&lt;/code&gt; inside.&lt;/p&gt;
&lt;p&gt;Next, in the Firebase console, go through the process of creating a new “web project” for your application. Once setup, navigate to the “General” tab of your “Project Settings” and select the newly-created Web App. In the “SDK setup and configuration” section, select “Config” and copy the displayed &lt;code&gt;firebaseConfig&lt;/code&gt; object as shown in the image below.&lt;/p&gt;
&lt;p&gt;&lt;img src=&#34;https://wilw.dev/media/blog/firebase-1.png&#34; alt=&#34;The Firebase console, displaying the SDK setup details&#34;&gt;&lt;/p&gt;
&lt;p&gt;Create a new empty service worker file named &lt;code&gt;firebase-messaging-sw.js&lt;/code&gt; in the &lt;code&gt;web/&lt;/code&gt; directory of your Flutter project and paste the &lt;code&gt;firebaseConfig&lt;/code&gt; object into this new file:&lt;/p&gt;
&lt;div class=&#34;highlight&#34;&gt;&lt;pre tabindex=&#34;0&#34; style=&#34;background-color:#f0f3f3;-moz-tab-size:2;-o-tab-size:2;tab-size:2;&#34;&gt;&lt;code class=&#34;language-javascript&#34; data-lang=&#34;javascript&#34;&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;importScripts(&lt;span style=&#34;color:#c30&#34;&gt;&#39;https://www.gstatic.com/firebasejs/8.6.1/firebase-app.js&#39;&lt;/span&gt;);
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;importScripts(&lt;span style=&#34;color:#c30&#34;&gt;&#39;https://www.gstatic.com/firebasejs/8.6.1/firebase-messaging.js&#39;&lt;/span&gt;);
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;&lt;span style=&#34;color:#069;font-weight:bold&#34;&gt;const&lt;/span&gt; firebaseConfig &lt;span style=&#34;color:#555&#34;&gt;=&lt;/span&gt; {
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;  &lt;span style=&#34;color:#09f;font-style:italic&#34;&gt;// Copy from Firebase as described above
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;&lt;span style=&#34;color:#09f;font-style:italic&#34;&gt;&lt;/span&gt;};
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;firebase.initializeApp(firebaseConfig);
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;We now need to reference and register this service worker. To do so, open up the automatically-generated &lt;code&gt;web/index.html&lt;/code&gt; file in your Flutter project and add this &lt;code&gt;script&lt;/code&gt; block:&lt;/p&gt;
&lt;div class=&#34;highlight&#34;&gt;&lt;pre tabindex=&#34;0&#34; style=&#34;background-color:#f0f3f3;-moz-tab-size:2;-o-tab-size:2;tab-size:2;&#34;&gt;&lt;code class=&#34;language-html&#34; data-lang=&#34;html&#34;&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;&lt;&lt;span style=&#34;color:#309;font-weight:bold&#34;&gt;script&lt;/span&gt;&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;  &lt;span style=&#34;color:#069;font-weight:bold&#34;&gt;if&lt;/span&gt; (&lt;span style=&#34;color:#c30&#34;&gt;&#39;serviceWorker&#39;&lt;/span&gt; &lt;span style=&#34;color:#069;font-weight:bold&#34;&gt;in&lt;/span&gt; navigator) {
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;    &lt;span style=&#34;color:#366&#34;&gt;window&lt;/span&gt;.addEventListener(&lt;span style=&#34;color:#c30&#34;&gt;&#39;load&#39;&lt;/span&gt;, &lt;span style=&#34;color:#069;font-weight:bold&#34;&gt;function&lt;/span&gt; () {
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;      navigator.serviceWorker.register(&lt;span style=&#34;color:#c30&#34;&gt;&#39;/firebase-messaging-sw.js&#39;&lt;/span&gt;);
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;    });
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;  }
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;&lt;/&lt;span style=&#34;color:#309;font-weight:bold&#34;&gt;script&lt;/span&gt;&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;Finally, we need to slightly alter the way we request push notification permission from the client to allow this to work in a browser-based context. To do so, we need to provide the browser with a &lt;em&gt;web certificate&lt;/em&gt;. Luckily, this is easy to do in Firebase.&lt;/p&gt;
&lt;p&gt;Back in the Firebase console navigate to the “Cloud Messaging” tab of your project’s settings. In the “Web configuration” section, create a new Web Push certificate and, once done, make a note of the key pair displayed:&lt;/p&gt;
&lt;p&gt;&lt;img src=&#34;https://wilw.dev/media/blog/firebase-2.png&#34; alt=&#34;The Firebase console, displaying the web push certificates&#34;&gt;&lt;/p&gt;
&lt;p&gt;Somewhere in your Flutter app’s Dart code, you probably have something along these lines when requesting push notification permission for your iOS apps:&lt;/p&gt;
&lt;div class=&#34;highlight&#34;&gt;&lt;pre tabindex=&#34;0&#34; style=&#34;background-color:#f0f3f3;-moz-tab-size:2;-o-tab-size:2;tab-size:2;&#34;&gt;&lt;code class=&#34;language-dart&#34; data-lang=&#34;dart&#34;&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;FirebaseMessaging _firebaseMessaging &lt;span style=&#34;color:#555&#34;&gt;=&lt;/span&gt; FirebaseMessaging.instance;
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;&lt;span style=&#34;color:#069;font-weight:bold&#34;&gt;await&lt;/span&gt; _firebaseMessaging.requestPermission(
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;  &lt;span style=&#34;color:#99f&#34;&gt;alert:&lt;/span&gt; &lt;span style=&#34;color:#069;font-weight:bold&#34;&gt;true&lt;/span&gt;,
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;  &lt;span style=&#34;color:#99f&#34;&gt;badge:&lt;/span&gt; &lt;span style=&#34;color:#069;font-weight:bold&#34;&gt;true&lt;/span&gt;,
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;  &lt;span style=&#34;color:#99f&#34;&gt;sound:&lt;/span&gt; &lt;span style=&#34;color:#069;font-weight:bold&#34;&gt;true&lt;/span&gt;,
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;);
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;&lt;span style=&#34;color:#078;font-weight:bold&#34;&gt;String&lt;/span&gt; _pushToken &lt;span style=&#34;color:#555&#34;&gt;=&lt;/span&gt; &lt;span style=&#34;color:#069;font-weight:bold&#34;&gt;await&lt;/span&gt; _firebaseMessaging.getToken();
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;Make a change to that final line such that we now pass in the keypair you just created in Firebase:&lt;/p&gt;
&lt;div class=&#34;highlight&#34;&gt;&lt;pre tabindex=&#34;0&#34; style=&#34;background-color:#f0f3f3;-moz-tab-size:2;-o-tab-size:2;tab-size:2;&#34;&gt;&lt;code class=&#34;language-dart&#34; data-lang=&#34;dart&#34;&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;&lt;span style=&#34;color:#09f;font-style:italic&#34;&gt;// ... Previous code
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;&lt;span style=&#34;color:#09f;font-style:italic&#34;&gt;&lt;/span&gt;&lt;span style=&#34;color:#078;font-weight:bold&#34;&gt;String&lt;/span&gt; _pushToken &lt;span style=&#34;color:#555&#34;&gt;=&lt;/span&gt; &lt;span style=&#34;color:#069;font-weight:bold&#34;&gt;await&lt;/span&gt; _firebaseMessaging.getToken(
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;  &lt;span style=&#34;color:#99f&#34;&gt;vapidKey:&lt;/span&gt; &lt;span style=&#34;color:#c30&#34;&gt;&#39;&lt;KEYPAIR&gt;&#39;&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;);
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;And that’s it! Your application will now request permission for push notifications when it is delivered over a secure connection. Build the web version of your Flutter app (&lt;code&gt;flutter build web&lt;/code&gt;) and then deploy the &lt;code&gt;web/&lt;/code&gt; directory to your host of choice.&lt;/p&gt;
&lt;p&gt;&lt;img src=&#34;https://wilw.dev/media/blog/firebase-3.png&#34; alt=&#34;Firefox asking me if I want to allow push notifications on my Flutter app&#34;&gt;&lt;/p&gt;
&lt;p&gt;If permission is granted then the browser will receive and display notifications when they are sent - even if the browser tab for your app is currently closed! The system should also function on supported mobile browsers too, though please take a look through &lt;em&gt;Caveats&lt;/em&gt; below.&lt;/p&gt;
&lt;h2 id=&#34;caveats&#34;&gt;Caveats&lt;/h2&gt;
&lt;p&gt;Although browser web push is gaining &lt;a href=&#34;https://developer.mozilla.org/en-US/docs/Web/API/Push_API#browser_compatibility&#34; target=&#34;_blank&#34; rel=&#34;noopener noreferrer&#34; &gt;more and more support&lt;/a&gt; (and is already available in nearly all modern desktop browsers), they aren’t yet ubiquitously available. &lt;em&gt;Lookin’ at you, Safari.&lt;/em&gt;&lt;/p&gt;
&lt;p&gt;On Safari (either on my Mac or iPhone) the request permission flow would cause the app to throw an error and hang. To alleviate this I wrap the Firebase block in a &lt;code&gt;try/catch&lt;/code&gt; and show a dialog to warn the user when push is unavailable.&lt;/p&gt;
&lt;div class=&#34;highlight&#34;&gt;&lt;pre tabindex=&#34;0&#34; style=&#34;background-color:#f0f3f3;-moz-tab-size:2;-o-tab-size:2;tab-size:2;&#34;&gt;&lt;code class=&#34;language-dart&#34; data-lang=&#34;dart&#34;&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;&lt;span style=&#34;color:#069;font-weight:bold&#34;&gt;try&lt;/span&gt; {
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;  &lt;span style=&#34;color:#09f;font-style:italic&#34;&gt;// Firebase init and getToken code
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;&lt;span style=&#34;color:#09f;font-style:italic&#34;&gt;&lt;/span&gt;}
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;&lt;span style=&#34;color:#069;font-weight:bold&#34;&gt;catch&lt;/span&gt; (error) {
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;  showCupertinoDialog(
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;    &lt;span style=&#34;color:#99f&#34;&gt;context:&lt;/span&gt; context,
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;    &lt;span style=&#34;color:#99f&#34;&gt;builder:&lt;/span&gt; (BuildContext modalContext) &lt;span style=&#34;color:#555&#34;&gt;=&gt;&lt;/span&gt; CupertinoAlertDialog(
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;      &lt;span style=&#34;color:#99f&#34;&gt;title:&lt;/span&gt; &lt;span style=&#34;color:#069;font-weight:bold&#34;&gt;new&lt;/span&gt; Text(&lt;span style=&#34;color:#c30&#34;&gt;&#39;Unable to request push permissions&#39;&lt;/span&gt;),
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;      &lt;span style=&#34;color:#99f&#34;&gt;content:&lt;/span&gt; &lt;span style=&#34;color:#069;font-weight:bold&#34;&gt;new&lt;/span&gt; Text(&lt;span style=&#34;color:#c30&#34;&gt;&#39;Push may not be supported on your device.&#39;&lt;/span&gt;),
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;      &lt;span style=&#34;color:#99f&#34;&gt;actions:&lt;/span&gt; &lt;span style=&#34;color:#555&#34;&gt;&lt;&lt;/span&gt;Widget&lt;span style=&#34;color:#555&#34;&gt;&gt;&lt;/span&gt;[
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;        CupertinoDialogAction(
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;          &lt;span style=&#34;color:#99f&#34;&gt;isDefaultAction:&lt;/span&gt; &lt;span style=&#34;color:#069;font-weight:bold&#34;&gt;true&lt;/span&gt;,
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;          &lt;span style=&#34;color:#99f&#34;&gt;child:&lt;/span&gt; Text(&lt;span style=&#34;color:#c30&#34;&gt;&#39;OK&#39;&lt;/span&gt;),
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;          &lt;span style=&#34;color:#99f&#34;&gt;onPressed:&lt;/span&gt; () &lt;span style=&#34;color:#555&#34;&gt;=&gt;&lt;/span&gt; Navigator.of(modalContext).pop(),
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;        ),
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;      ],
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;    ),
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;  );
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;}
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;There may be a more elegant solution to detect this up-front, but I was unable to find one that worked nicely across web and mobile. Please let me know if there is a better approach to handling Safari and other unsupported browsers!&lt;/p&gt;
&lt;h2 id=&#34;conclusion&#34;&gt;Conclusion&lt;/h2&gt;
&lt;p&gt;In this post I’ve briefly walked through the set-up of Firebase push notifications in a Flutter web app, with an approach that should allow you to maintain a single codebase and maintain functionality across web and native mobile app builds.&lt;/p&gt;
&lt;p&gt;Although I enjoy using it, I feel that I’m still relatively inexperienced with Flutter. I am continually learning about new and interesting concepts and I’ll try and cover more about these as I discover them!&lt;/p&gt;
</content:encoded>
    </item>
    
    <item>
      <title>Moving my Matrix identity to Element One</title>
      <link>https://wilw.dev/blog/2021/12/15/element-one/</link>
      <pubDate>Wed, 15 Dec 2021 21:51:00 +0000</pubDate>
      
      <guid>https://wilw.dev/blog/2021/12/15/element-one/</guid>
      
        <category>100daystooffload</category>
      
        <category>technology</category>
      
        <category>selfhost</category>
      
      
      <content:encoded>&lt;p&gt;For as long as I’ve been using &lt;a href=&#34;https://matrix.org&#34; target=&#34;_blank&#34; rel=&#34;noopener noreferrer&#34; &gt;Matrix&lt;/a&gt; I’ve hosted my own homeserver on my own VPS and at my own domain.&lt;/p&gt;
&lt;h2 id=&#34;why-i-chose-to-move&#34;&gt;Why I chose to move&lt;/h2&gt;
&lt;p&gt;I previously &lt;a href=&#34;https://wilw.dev/blog/2021/03/22/host-matrix&#34;&gt;wrote about&lt;/a&gt; how I self-host my homeserver with the help of the &lt;a href=&#34;https://github.com/matrix-org/synapse&#34; target=&#34;_blank&#34; rel=&#34;noopener noreferrer&#34; &gt;Synapse project&lt;/a&gt;. Although this set-up is quite straight forward, it’s an extra system to maintain with all of the associated overheads.&lt;/p&gt;
&lt;p&gt;One of the reasons I don’t host my own mail server is that I fear missed messages and silent bounces. I trust dedicated mail providers (particularly &lt;a href=&#34;https://fastmail.com&#34; target=&#34;_blank&#34; rel=&#34;noopener noreferrer&#34; &gt;Fastmail&lt;/a&gt;) more than myself in providing a robust enough service to ensure things get through. Equally, if I am telling other people my Matrix handle, then I want to make sure that messages they send (and those that I send) actually get delivered without any problems.&lt;/p&gt;
&lt;p&gt;The Matrix project, and &lt;a href=&#34;https://element.io&#34; target=&#34;_blank&#34; rel=&#34;noopener noreferrer&#34; &gt;Element&lt;/a&gt; too, are moving at such a pace that my own setup quickly feels old-fashioned. They also introduce new features for which I would need to take time learning about and setting-up in a robust way.&lt;/p&gt;
&lt;p&gt;So, last week, I made the decision to move my primary Matrix identity over to the &lt;a href=&#34;https://element.io/element-one&#34; target=&#34;_blank&#34; rel=&#34;noopener noreferrer&#34; &gt;Element One&lt;/a&gt; service. I pay $5 a month for their basic plan.&lt;/p&gt;
&lt;h2 id=&#34;the-migration-process&#34;&gt;The migration process&lt;/h2&gt;
&lt;p&gt;Moving was relatively simple. I just created a new account on the Element One website and, once in, I re-joined the rooms I had previously been in on my old server. I also logged-into my new account on the Element One homeserver in my Element apps.&lt;/p&gt;
&lt;p&gt;I then messaged those people that I talk to frequently to let them know my new username. Unlike Mastodon, there is no way to seamlessly transfer and redirect conversations between homeservers, so this was a bit of a manual job (though not too time-consuming).&lt;/p&gt;
&lt;p&gt;I am keeping my old homeserver up for a few weeks in case any new messages come through, but will eventually archive the messages and shut this down.&lt;/p&gt;
&lt;h2 id=&#34;my-thoughts-so-far&#34;&gt;My thoughts so far&lt;/h2&gt;
&lt;p&gt;I’m really enjoying the fact that I don’t need to worry about the ongoing maintenance of my personal homeserver, and the knowledge that I can trust Element in that they know what they’re doing.&lt;/p&gt;
&lt;p&gt;The bridges that come with Element One - for Telegram, Whatsapp, and Signal - are fantastic. They work seamlessly; I get read receipts, can reply to messages, and see when people are typing. Contact profile information (such as avatar images) get pulled through too with no problems.&lt;/p&gt;
&lt;p&gt;This means I can chat to people and groups on Matrix, Telegram, and Whatsapp all from one place, which is super convenient! Sure - the chats are no longer end-to-end encrypted (yet), but I rarely use secure chats on Telegram anyway and Whatsapp’s claims have always been spurious.&lt;/p&gt;
&lt;p&gt;On the flip-side, Element One restricts you to their own domain name for their homeserver. As such, my new handle is &lt;code&gt;@wilw:one.ems.host&lt;/code&gt;, and I’ve lost the “vanity” associated with using my own domain. This also means that I need to go through the migration process again if I ever move away from Element.&lt;/p&gt;
&lt;p&gt;&lt;a href=&#34;https://element.io/matrix-services&#34; target=&#34;_blank&#34; rel=&#34;noopener noreferrer&#34; &gt;EMS (Element Matrix Services)&lt;/a&gt; does offer the ability to bring your own domain, but this service is aimed more at groups or businesses, since the smallest plan comes with a minimum of 5 users at $3 each. For now, this is overkill for just me.&lt;/p&gt;
&lt;p&gt;This is the only down-side I notice at the moment, and it isn’t a big killer for me since most of my activity is room-based anyway.&lt;/p&gt;
&lt;h2 id=&#34;conclusion&#34;&gt;Conclusion&lt;/h2&gt;
&lt;p&gt;In general, I’m glad to have made the move. Things work smoothly, I get to use modern Matrix features, and I trust the robustness and security of the professionally-hosted platform.&lt;/p&gt;
&lt;p&gt;I’m not emotionally tied to Element One, and am always interested to hear about other Matrix homeservers and communities that may be more of a fit for me. However, for now, it’s a great alternative to self-hosting.&lt;/p&gt;
</content:encoded>
    </item>
    
    <item>
      <title>Nextcloud for photos: using cheaper object storage</title>
      <link>https://wilw.dev/blog/2021/12/11/nextcloud-object-storage/</link>
      <pubDate>Sat, 11 Dec 2021 10:54:00 +0000</pubDate>
      
      <guid>https://wilw.dev/blog/2021/12/11/nextcloud-object-storage/</guid>
      
        <category>100daystooffload</category>
      
        <category>technology</category>
      
        <category>selfhost</category>
      
      
      <content:encoded>&lt;p&gt;Some people may remember my quest a few months back to &lt;a href=&#34;https://wilw.dev/blog/2021/02/24/google-photos-pcloud&#34;&gt;find a good alternative to Google Photos&lt;/a&gt; for image storage and backup.&lt;/p&gt;
&lt;p&gt;At the time, I talked about Piwigo, Mega and pCloud as potential candidates. I also briefly touched upon Nextcloud in that post - a service I use (and self-host) anyway for all of my other storage needs, but I did not consider it further due to the high cost of the associated block storage required to house a large number of images.&lt;/p&gt;
&lt;p&gt;In this post I want to explore a different approach to storing files using Nextcloud: one that uses &lt;em&gt;object storage&lt;/em&gt; (instead of block storage) as a backend.&lt;/p&gt;
&lt;h2 id=&#34;briefly-block-storage-vs-object-storage&#34;&gt;Briefly: block storage vs object storage&lt;/h2&gt;
&lt;p&gt;This is a fairly obvious point these days in the cloud world, but I thought I’d include it in case the reader isn’t familiar with these terms.&lt;/p&gt;
&lt;p&gt;&lt;em&gt;Block storage&lt;/em&gt; is a facility offered by cloud and infrastructure-as-a-service (IaaS) providers. It is a system that enables you to create &lt;em&gt;volumes&lt;/em&gt; that can then be attached and mounted by your cloud virtual machines. Once attached, they can be formatted with a filesystem of your choice and used just like any other filesystem mounted by your machine.&lt;/p&gt;
&lt;p&gt;Block storage volumes can often be detached and re-attached to other instances, or even persist without being attached to any instance. Most modern providers use high-performance SSDs to power this facility, making it a great option for data-processing on cloud servers.&lt;/p&gt;
&lt;p&gt;&lt;em&gt;Object storage&lt;/em&gt;, on the other hand, is more of a &lt;em&gt;service&lt;/em&gt; offered by the cloud provider. Users can create containers known as &lt;em&gt;buckets&lt;/em&gt;, and in these store any number of arbitrary files (“objects”). Buckets don’t get attached to instances, and the objects they contain can be accessed completely independently over the web or via SDKs.&lt;/p&gt;
&lt;p&gt;As such, object storage can be super useful for storing and retrieving user-uploaded content in a service, or for storing large collections of arbitrary data.&lt;/p&gt;
&lt;p&gt;From the viewpoint of this post, the key difference between the two is the &lt;em&gt;pricing&lt;/em&gt;. I use Linode as my cloud provider of choice, and - to give you an idea of the scale - 1TB of &lt;a href=&#34;https://www.linode.com/products/block-storage&#34; target=&#34;_blank&#34; rel=&#34;noopener noreferrer&#34; &gt;block storage&lt;/a&gt; would cost you over $100 a month. The same amount on &lt;a href=&#34;https://www.linode.com/products/object-storage&#34; target=&#34;_blank&#34; rel=&#34;noopener noreferrer&#34; &gt;object storage&lt;/a&gt; would be $20 a month.&lt;/p&gt;
&lt;p&gt;Another factor to consider is that block storage must be pre-provisioned. I.e., once you create the volume you begin to be charged even if the volume is unformatted and empty. In object storage the cost scales depending on the amount of data actually contained by your buckets.&lt;/p&gt;
&lt;h2 id=&#34;object-storage-competing-with-the-photos-alternatives&#34;&gt;Object storage: competing with the photos alternatives&lt;/h2&gt;
&lt;p&gt;Based on the above, object storage is naturally a sensible choice if one were to choose out of the two for storing large volumes of images, since it’s the one that competes the most with other commercial offerings in terms of pricing.&lt;/p&gt;
&lt;p&gt;Neither &lt;a href=&#34;https://support.apple.com/en-us/HT201238&#34; target=&#34;_blank&#34; rel=&#34;noopener noreferrer&#34; &gt;Apple iCloud&lt;/a&gt; or &lt;a href=&#34;https://one.google.com/about&#34; target=&#34;_blank&#34; rel=&#34;noopener noreferrer&#34; &gt;Google Photos&lt;/a&gt; offer a 1TB option, but their 2TB storage options are priced at around $10 a month.&lt;/p&gt;
&lt;p&gt;Whilst this is essentially quarter the price of the Linode object storage pricing (which, in itself is great), the commercial offerings must also be pre-provisioned (i.e. you pay that amount even if you only upload one photo). You also become more locked into their relevant walled gardens, with all of the limitations &lt;a href=&#34;https://wilw.dev/blog/2021/02/24/google-photos-pcloud&#34;&gt;I spoke about previously&lt;/a&gt;.&lt;/p&gt;
&lt;p&gt;As such, I believe that a system based on your own managed block storage solution is a viable alternative.&lt;/p&gt;
&lt;h2 id=&#34;nextcloud-for-image-storage&#34;&gt;Nextcloud for image storage&lt;/h2&gt;
&lt;p&gt;&lt;a href=&#34;https://nextcloud.com&#34; target=&#34;_blank&#34; rel=&#34;noopener noreferrer&#34; &gt;Nextcloud&lt;/a&gt; offers a great web interface and mobile app for accessing your files and viewing stored images. The mobile app also has an “auto-upload” facility for photos, which is super useful.&lt;/p&gt;
&lt;p&gt;This, combined with the object storage, provides a more cost-effective way of managing files and photos stored in your Nextcloud.&lt;/p&gt;
&lt;p&gt;Some people have reported issues with Nextcloud when viewing large folders of images (since the thumbnail-generating task is quite CPU-intensive). However, I am keen to give this a go to see if it could be a viable option.&lt;/p&gt;
&lt;h2 id=&#34;configuring-nextcloud-to-use-object-storage&#34;&gt;Configuring Nextcloud to use object storage&lt;/h2&gt;
&lt;p&gt;To set-up a Nextcloud to use object storage I recommend creating a fresh new instance, since changing your existing one will make your previously-stored files unavailable.&lt;/p&gt;
&lt;h3 id=&#34;create-a-bucket-and-access-credentials&#34;&gt;Create a bucket and access credentials&lt;/h3&gt;
&lt;p&gt;In this post I will use Linode’s object storage as an example, but the process should be similar for any S3-compatible backend (e.g. Amazon S3, DigitalOcean Spaces, Backblaze B2, etc.).&lt;/p&gt;
&lt;p&gt;To begin, login to the Linode dashboard and navigate to the “Object Storage” page. Then click “Create Bucket”, give your new bucket a name and choose a geographic region for its storage.&lt;/p&gt;
&lt;p&gt;&lt;img src=&#34;https://wilw.dev/media/blog/linode_object_1.png&#34; alt=&#34;Creating a new bucket in the Linode console&#34;&gt;&lt;/p&gt;
&lt;p&gt;Next, navigate to the “Access Keys” tab on the “Object Storage” page and click “Create Access Key”. Give your key a label to remember it by, and select to give it limited access such that it can only read/write to the bucket you just created.&lt;/p&gt;
&lt;p&gt;&lt;img src=&#34;https://wilw.dev/media/blog/linode_object_2.png&#34; alt=&#34;Creating an access key for the new bucket&#34;&gt;&lt;/p&gt;
&lt;p&gt;When created, a one-time dialog will popup to display the details of the new key. Make a note of both the “Access Key” and the “Secret Key”, and then close the dialog.&lt;/p&gt;
&lt;p&gt;At this stage, you should now have:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;A bucket name&lt;/li&gt;
&lt;li&gt;An access key&lt;/li&gt;
&lt;li&gt;An access key secret&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;You can now close the Linode dashboard.&lt;/p&gt;
&lt;h3 id=&#34;creating-a-new-nextcloud-instance&#34;&gt;Creating a new Nextcloud instance&lt;/h3&gt;
&lt;p&gt;I use Docker to run my services, so I recommend creating a new &lt;code&gt;docker-compose.yml&lt;/code&gt; file with these contents:&lt;/p&gt;
&lt;div class=&#34;highlight&#34;&gt;&lt;pre tabindex=&#34;0&#34; style=&#34;background-color:#f0f3f3;-moz-tab-size:2;-o-tab-size:2;tab-size:2;&#34;&gt;&lt;code class=&#34;language-yml&#34; data-lang=&#34;yml&#34;&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;&lt;span style=&#34;color:#309;font-weight:bold&#34;&gt;version&lt;/span&gt;:&lt;span style=&#34;color:#bbb&#34;&gt; &lt;/span&gt;&lt;span style=&#34;color:#c30&#34;&gt;&#39;2&#39;&lt;/span&gt;&lt;span style=&#34;color:#bbb&#34;&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;&lt;span style=&#34;color:#bbb&#34;&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;&lt;span style=&#34;color:#bbb&#34;&gt;&lt;/span&gt;&lt;span style=&#34;color:#309;font-weight:bold&#34;&gt;services&lt;/span&gt;:&lt;span style=&#34;color:#bbb&#34;&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;&lt;span style=&#34;color:#bbb&#34;&gt;  &lt;/span&gt;&lt;span style=&#34;color:#309;font-weight:bold&#34;&gt;nextcloud&lt;/span&gt;:&lt;span style=&#34;color:#bbb&#34;&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;&lt;span style=&#34;color:#bbb&#34;&gt;    &lt;/span&gt;&lt;span style=&#34;color:#309;font-weight:bold&#34;&gt;image&lt;/span&gt;:&lt;span style=&#34;color:#bbb&#34;&gt; &lt;/span&gt;nextcloud:23&lt;span style=&#34;color:#bbb&#34;&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;&lt;span style=&#34;color:#bbb&#34;&gt;    &lt;/span&gt;&lt;span style=&#34;color:#309;font-weight:bold&#34;&gt;volumes&lt;/span&gt;:&lt;span style=&#34;color:#bbb&#34;&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;&lt;span style=&#34;color:#bbb&#34;&gt;      &lt;/span&gt;- ./nextcloud-data/storage:/var/www/html&lt;span style=&#34;color:#bbb&#34;&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;&lt;span style=&#34;color:#bbb&#34;&gt;    &lt;/span&gt;&lt;span style=&#34;color:#309;font-weight:bold&#34;&gt;restart&lt;/span&gt;:&lt;span style=&#34;color:#bbb&#34;&gt; &lt;/span&gt;always&lt;span style=&#34;color:#bbb&#34;&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;Feel free to add any network configurations as required (e.g. to an Nginx host for TLS termination) and then bring up your instance: &lt;code&gt;docker-compose up -d&lt;/code&gt;. You can also specify a database container in your compose file if the default SQLite option isn’t appropriate for your needs.&lt;/p&gt;
&lt;h3 id=&#34;configuring-nextcloud-to-use-your-new-bucket&#34;&gt;Configuring Nextcloud to use your new bucket&lt;/h3&gt;
&lt;p&gt;Once up and running, Nextcloud should create its files and directories in the &lt;code&gt;nextcloud-data&lt;/code&gt; directory. To configure your instance to use object storage for its primary storage, you’ll need to add some lines to &lt;code&gt;nextcloud-data/storage/config/config.php&lt;/code&gt;:&lt;/p&gt;
&lt;div class=&#34;highlight&#34;&gt;&lt;pre tabindex=&#34;0&#34; style=&#34;background-color:#f0f3f3;-moz-tab-size:2;-o-tab-size:2;tab-size:2;&#34;&gt;&lt;code class=&#34;language-php&#34; data-lang=&#34;php&#34;&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;&lt;span style=&#34;color:#555&#34;&gt;&lt;?&lt;/span&gt;php
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;&lt;span style=&#34;color:#033&#34;&gt;$CONFIG&lt;/span&gt; &lt;span style=&#34;color:#555&#34;&gt;=&lt;/span&gt; &lt;span style=&#34;color:#069;font-weight:bold&#34;&gt;array&lt;/span&gt; (
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;  &lt;span style=&#34;color:#555&#34;&gt;...&lt;/span&gt; &lt;span style=&#34;color:#09f;font-style:italic&#34;&gt;// Omitted for brevity
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;&lt;span style=&#34;color:#09f;font-style:italic&#34;&gt;&lt;/span&gt;  &lt;span style=&#34;color:#c30&#34;&gt;&#39;objectstore&#39;&lt;/span&gt; &lt;span style=&#34;color:#555&#34;&gt;=&gt;&lt;/span&gt; [
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;    &lt;span style=&#34;color:#c30&#34;&gt;&#39;class&#39;&lt;/span&gt; &lt;span style=&#34;color:#555&#34;&gt;=&gt;&lt;/span&gt; &lt;span style=&#34;color:#c30&#34;&gt;&#39;\\OC\\Files\\ObjectStore\\S3&#39;&lt;/span&gt;,
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;    &lt;span style=&#34;color:#c30&#34;&gt;&#39;arguments&#39;&lt;/span&gt; &lt;span style=&#34;color:#555&#34;&gt;=&gt;&lt;/span&gt; [
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;      &lt;span style=&#34;color:#c30&#34;&gt;&#39;bucket&#39;&lt;/span&gt; &lt;span style=&#34;color:#555&#34;&gt;=&gt;&lt;/span&gt; &lt;span style=&#34;color:#c30&#34;&gt;&#39;BUCKET_NAME&#39;&lt;/span&gt;,
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;      &lt;span style=&#34;color:#c30&#34;&gt;&#39;key&#39;&lt;/span&gt;    &lt;span style=&#34;color:#555&#34;&gt;=&gt;&lt;/span&gt; &lt;span style=&#34;color:#c30&#34;&gt;&#39;ACCESS_KEY&#39;&lt;/span&gt;,
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;      &lt;span style=&#34;color:#c30&#34;&gt;&#39;secret&#39;&lt;/span&gt; &lt;span style=&#34;color:#555&#34;&gt;=&gt;&lt;/span&gt; &lt;span style=&#34;color:#c30&#34;&gt;&#39;ACCESS_KEY_SECRET&#39;&lt;/span&gt;,
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;      &lt;span style=&#34;color:#c30&#34;&gt;&#39;hostname&#39;&lt;/span&gt; &lt;span style=&#34;color:#555&#34;&gt;=&gt;&lt;/span&gt; &lt;span style=&#34;color:#c30&#34;&gt;&#39;eu-central-1.linodeobjects.com&#39;&lt;/span&gt;,
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;      &lt;span style=&#34;color:#c30&#34;&gt;&#39;port&#39;&lt;/span&gt; &lt;span style=&#34;color:#555&#34;&gt;=&gt;&lt;/span&gt; &lt;span style=&#34;color:#f60&#34;&gt;443&lt;/span&gt;,
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;      &lt;span style=&#34;color:#c30&#34;&gt;&#39;use_ssl&#39;&lt;/span&gt; &lt;span style=&#34;color:#555&#34;&gt;=&gt;&lt;/span&gt; &lt;span style=&#34;color:#069;font-weight:bold&#34;&gt;true&lt;/span&gt;,
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;    ],
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;  ],
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;  &lt;span style=&#34;color:#555&#34;&gt;...&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;);
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;You’ll need to change the &lt;code&gt;BUCKET_NAME&lt;/code&gt;, &lt;code&gt;ACCESS_KEY&lt;/code&gt;, and &lt;code&gt;ACCESS_KEY_SECRET&lt;/code&gt; to the values you noted earlier. You may also need to change the &lt;code&gt;hostname&lt;/code&gt; if you use a different storage region or a different provider.&lt;/p&gt;
&lt;p&gt;You can now save and exit the file.&lt;/p&gt;
&lt;h3 id=&#34;testing-the-set-up&#34;&gt;Testing the set-up&lt;/h3&gt;
&lt;p&gt;To test the set-up, navigate to the URL you’ve configured for your instance. If this is the first time you’ve visited it there will be a few bits of setting-up to do, which Nextcloud will guide you through.&lt;/p&gt;
&lt;p&gt;Once you’re through and logged-in, you can try adding files using the web interface and then use the Linode dashboard to verify that Nextcloud has created new files in the bucket. Note that the file names and extensions will be changed, since Nextcloud manages metadata and naming in its own database.&lt;/p&gt;
&lt;p&gt;If something goes wrong then just check the logs of your running Nextcloud container: &lt;code&gt;docker-compose logs&lt;/code&gt;.&lt;/p&gt;
&lt;h3 id=&#34;enabling-automatic-photo-upload&#34;&gt;Enabling automatic photo upload&lt;/h3&gt;
&lt;p&gt;Finally, when you’re ready, you can try importing your photos or enabling the Nextcloud app auto-upload feature on your phone. This feature can be found under the app’s settings.&lt;/p&gt;
&lt;p&gt;&lt;img src=&#34;https://wilw.dev/media/blog/linode_object_3.jpeg&#34; alt=&#34;Nextcloud settings screen, showing Auto-Upload at the top&#34;&gt;&lt;/p&gt;
&lt;p&gt;I recommend trying it for a while and seeing how you get on with regards to performance and stability.&lt;/p&gt;
&lt;h2 id=&#34;wrapping-up&#34;&gt;Wrapping-up&lt;/h2&gt;
&lt;p&gt;In this post I’ve covered the advantages of object storage, and how this mechanism can be applied to Nextcloud to provide a self-hosted alternative to Google and Apple photos.&lt;/p&gt;
&lt;p&gt;Personally, I am still not 100% on the performance of Nextcloud in terms of photo-viewing. However, I would like to give it a chance and it could be viable anyway as a secondary backup to an existing primary system.&lt;/p&gt;
&lt;p&gt;Google’s own image search and navigation is so convenient that creating a real competitor is difficult. However, I am keen to hear how others get on in this space!&lt;/p&gt;
</content:encoded>
    </item>
    
    <item>
      <title>The Idiot Brain by Dean Burnett</title>
      <link>https://wilw.dev/blog/2021/12/08/idiot-brain/</link>
      <pubDate>Wed, 08 Dec 2021 18:33:00 +0000</pubDate>
      
      <guid>https://wilw.dev/blog/2021/12/08/idiot-brain/</guid>
      
        <category>100daystooffload</category>
      
        <category>book</category>
      
      
      <content:encoded>&lt;p&gt;&lt;a href=&#34;https://www.goodreads.com/author/show/14232360.Dean_Burnett&#34; target=&#34;_blank&#34; rel=&#34;noopener noreferrer&#34; &gt;Dean Burnett&lt;/a&gt;’s &lt;a href=&#34;https://www.goodreads.com/book/show/32191721-idiot-brain&#34; target=&#34;_blank&#34; rel=&#34;noopener noreferrer&#34; &gt;The Idiot Brain&lt;/a&gt; is an interesting insight into why people think the way they do, personality, emotion, and the biology of the brain.&lt;/p&gt;
&lt;p&gt;&lt;img src=&#34;https://wilw.dev/media/blog/idiot_brain.jpg&#34; alt=&#34;The Idiot Brain cover&#34;&gt;&lt;/p&gt;
&lt;p&gt;The author (who happens to live in the same city as me: Cardiff), covers a wide range of examples of human behaviour and relates them to brain function. Often these are based on defence mechanisms developed over the vast time of human evolution, and it’s amazing how our perception of fear and “uncertainty” can have an impact on other feelings and emotions too - such as embarrassment.&lt;/p&gt;
&lt;p&gt;I really enjoyed learning about how seemingly-complex traits and personalities in other people can be driven by relatively simple innate characteristics and biology, and how relationships between traits can form - such as how more intelligent people are typically less confident.&lt;/p&gt;
&lt;p&gt;I was also surprised by how much we - as humans - have come to understand about the brain, although of course there is still much to be discovered.&lt;/p&gt;
&lt;p&gt;The book is well-written and uses very witty examples throughout. It’s a great read and I can certainly recommend it.&lt;/p&gt;
</content:encoded>
    </item>
    
    <item>
      <title>Parse and process incoming emails in a web application</title>
      <link>https://wilw.dev/blog/2021/12/05/handling-incoming-mail/</link>
      <pubDate>Sun, 05 Dec 2021 13:15:00 +0000</pubDate>
      
      <guid>https://wilw.dev/blog/2021/12/05/handling-incoming-mail/</guid>
      
        <category>100daystooffload</category>
      
        <category>technology</category>
      
      
      <content:encoded>&lt;p&gt;Most applications include some sort of outbound transactional email as part of their normal function. These email messages could be to support account-level features (such as password-resets) or to notify the user about activity relevant to them.&lt;/p&gt;
&lt;p&gt;In the latter case, such emails might be read, and then archived or deleted by the user, without further direct action. They aren’t typically designed to be something one actions or replies to - they’re mostly there to bring you back into engaging with the platform.&lt;/p&gt;
&lt;p&gt;However, what if your users could interact with your platform via email to some extent? For example, if someone were to send you a message on the platform - and you receive an email notifying you about the message, along with its contents - then the natural thing would be for you to be able to click “reply” to the email and write a message back.&lt;/p&gt;
&lt;p&gt;It is probably kinder to the user to provide this option, rather than always sending from a &lt;code&gt;noreply@&lt;/code&gt; type address (since everyone has email clients wherever they go), and it also means that your service sees more interaction (even if indirectly).&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;In this post I will cover an approach to enabling users to be able to simply click “reply” to transactional emails, and to allow the replies to be processed by the platform.&lt;/strong&gt;&lt;/p&gt;
&lt;h2 id=&#34;transmitting-and-receiving-emails&#34;&gt;Transmitting and receiving emails&lt;/h2&gt;
&lt;p&gt;Luckily, many current transactional email providers make it quite straight-forward to support this sort of functionality.&lt;/p&gt;
&lt;p&gt;I will talk about &lt;a href=&#34;https://sendgrid.com&#34; target=&#34;_blank&#34; rel=&#34;noopener noreferrer&#34; &gt;Sendgrid&lt;/a&gt; as a transactional mail provider for the benefit of this post, and will use a Python Flask app demo for handling the inbound webhooks from Sendgrid.&lt;/p&gt;
&lt;p&gt;Essentially, the flow will be the following:&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;User “A” sends user “B” a message in our platform&lt;/li&gt;
&lt;li&gt;The platform sends an email to user “B” containing the message, with a special &lt;code&gt;Reply-To&lt;/code&gt; header&lt;/li&gt;
&lt;li&gt;User “B” writes a message back to user “A” by directly replying to the email&lt;/li&gt;
&lt;li&gt;Sendgrid processes the incoming email and sends a &lt;code&gt;POST&lt;/code&gt; request with the details back to our platform&lt;/li&gt;
&lt;li&gt;Our platform receives the webhook from Sendgrid, reads the message and headers and takes action.&lt;/li&gt;
&lt;/ol&gt;
&lt;h3 id=&#34;configuring-the-transactional-mail-provider&#34;&gt;Configuring the transactional mail provider&lt;/h3&gt;
&lt;p&gt;The first thing to do is to set-up a mail provider we can use for outgoing and incoming email messages. As mentioned, in this post we’ll discuss Sendgrid, but others would also do the job (such as &lt;a href=&#34;https://www.mailgun.com&#34; target=&#34;_blank&#34; rel=&#34;noopener noreferrer&#34; &gt;Mailgun&lt;/a&gt;).&lt;/p&gt;
&lt;p&gt;Set up an account on Sendgrid and validate the domain name you’ll use to carry the email messages via your DNS manager. In this post we’ll assume the domain is &lt;code&gt;mail.example.com&lt;/code&gt;. Copy and paste the needed connection details (such as API tokens) and configure your app to send mail in the documented way (I won’t cover that in this post, as it is out of scope).&lt;/p&gt;
&lt;p&gt;You’ll also need to configure your domain’s DNS with an MX record to enable Sendgrid to receive email sent to addresses at your domain. The details are as follows:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;Host: &lt;code&gt;mail.example.com&lt;/code&gt; (replace this with your actual domain)&lt;/li&gt;
&lt;li&gt;Type: &lt;code&gt;MX&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;Server/target: &lt;code&gt;mx.sendgrid.net.&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;Priority: &lt;code&gt;10&lt;/code&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;Next, we need to tell Sendgrid to handle the inbound messages. For this, navigate to Settings -&gt; Inbound Parse on the Sendgrid dashboard (or &lt;a href=&#34;https://app.sendgrid.com/settings/parse&#34; target=&#34;_blank&#34; rel=&#34;noopener noreferrer&#34; &gt;use this direct link&lt;/a&gt;). Click “Add Host &amp; URL” and enter the details as needed. In the example below, I have configured Sendgrid to handle all email sent to &lt;code&gt;anything@mail.example.com&lt;/code&gt; and to &lt;code&gt;POST&lt;/code&gt; the parsed email to our app (&lt;a href=&#34;https://app.example.com&#34; target=&#34;_blank&#34; rel=&#34;noopener noreferrer&#34; &gt;https://app.example.com&lt;/a&gt;) at the &lt;code&gt;/webhooks/email&lt;/code&gt; route.&lt;/p&gt;
&lt;p&gt;&lt;img src=&#34;https://wilw.dev/media/blog/sendgrid1.png&#34; alt=&#34;Sendgrid Inbound Parse interface: creating a new webhook URL&#34;&gt;&lt;/p&gt;
&lt;p&gt;Once that’s done, we can close the Sendgrid manager.&lt;/p&gt;
&lt;h3 id=&#34;sending-notification-emails&#34;&gt;Sending notification emails&lt;/h3&gt;
&lt;p&gt;Next, we need to make a change to the way we send our notification emails to users. For now, we’ll just focus on the “new message” event, but the same process could also be applied to other types of events in our platform.&lt;/p&gt;
&lt;p&gt;The &lt;code&gt;Reply-To&lt;/code&gt; field of an email message contains an email address, and tells standards-compliant email clients to use this address (instead of the &lt;code&gt;From&lt;/code&gt; address) when users click the “Reply” button on the email.&lt;/p&gt;
&lt;p&gt;We will need to add this field to outbound emails notifying about new messages such that Sendgrid can pick them up and we can process them later. There are various ways to achieve this, depending on your existing approach to sending transactional mail, but the below is an example in Python (given we know the &lt;code&gt;message_id&lt;/code&gt; of the message we are notifying about):&lt;/p&gt;
&lt;div class=&#34;highlight&#34;&gt;&lt;pre tabindex=&#34;0&#34; style=&#34;background-color:#f0f3f3;-moz-tab-size:2;-o-tab-size:2;tab-size:2;&#34;&gt;&lt;code class=&#34;language-python&#34; data-lang=&#34;python&#34;&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;msg &lt;span style=&#34;color:#555&#34;&gt;=&lt;/span&gt; MIMEMultipart(&lt;span style=&#34;color:#c30&#34;&gt;&#39;alternative&#39;&lt;/span&gt;)
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;msg&lt;span style=&#34;color:#555&#34;&gt;.&lt;/span&gt;attach(MIMEText(text, &lt;span style=&#34;color:#c30&#34;&gt;&#39;plain&#39;&lt;/span&gt;)) &lt;span style=&#34;color:#09f;font-style:italic&#34;&gt;# add other fields to the email&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;msg[&lt;span style=&#34;color:#c30&#34;&gt;&#39;Subject&#39;&lt;/span&gt;] &lt;span style=&#34;color:#555&#34;&gt;=&lt;/span&gt; subject &lt;span style=&#34;color:#09f;font-style:italic&#34;&gt;# add a subject line&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;msg[&lt;span style=&#34;color:#c30&#34;&gt;&#39;To&#39;&lt;/span&gt;] &lt;span style=&#34;color:#555&#34;&gt;=&lt;/span&gt; &lt;span style=&#34;color:#c30&#34;&gt;&#39;, &#39;&lt;/span&gt;&lt;span style=&#34;color:#555&#34;&gt;.&lt;/span&gt;join(to) &lt;span style=&#34;color:#09f;font-style:italic&#34;&gt;# add &#34;to&#34; addresses (there can be more than one!)&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;msg[&lt;span style=&#34;color:#c30&#34;&gt;&#39;Reply-To&#39;&lt;/span&gt;] &lt;span style=&#34;color:#555&#34;&gt;=&lt;/span&gt; &lt;span style=&#34;color:#c30&#34;&gt;f&lt;/span&gt;&lt;span style=&#34;color:#c30&#34;&gt;&#39;message.&lt;/span&gt;&lt;span style=&#34;color:#a00&#34;&gt;{&lt;/span&gt;message_id&lt;span style=&#34;color:#a00&#34;&gt;}&lt;/span&gt;&lt;span style=&#34;color:#c30&#34;&gt;@mail.example.com&#39;&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;&lt;span style=&#34;color:#09f;font-style:italic&#34;&gt;# Continued mail logic...&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;In this example, we have specified a &lt;code&gt;Reply-To&lt;/code&gt; address that will look a little like this: &lt;code&gt;message.xxxyyyzzz@mail.example.com&lt;/code&gt;. This means that when a reply to this email comes in, we can use the &lt;code&gt;To&lt;/code&gt; field of the reply email to work out what the message is about - in this case a reply to the &lt;code&gt;message&lt;/code&gt; with an &lt;code&gt;id&lt;/code&gt; of &lt;code&gt;xxxyyyzzz&lt;/code&gt;. We’ll cover this later.&lt;/p&gt;
&lt;p&gt;Since we’ve already configured Sendgrid to receive all emails sent to &lt;code&gt;@mail.example.com&lt;/code&gt;, then we know that it will try and &lt;code&gt;POST&lt;/code&gt; the data back to our app once they’ve been received.&lt;/p&gt;
&lt;h3 id=&#34;handling-the-inbound-emails&#34;&gt;Handling the inbound emails&lt;/h3&gt;
&lt;p&gt;The final step is to ensure our app can handle the inbound webhooks from Sendgrid.&lt;/p&gt;
&lt;p&gt;As we saw earlier, we told Sendgrid to send the details of incoming emails to our app’s &lt;code&gt;/webhooks/email&lt;/code&gt; route, and so we will need to set that up. We also know that it will be a &lt;code&gt;POST&lt;/code&gt; request and the payload will be standard multipart form data.&lt;/p&gt;
&lt;p&gt;Below I have included an example of how we might handle this in a Python Flask app, but the same should be similar for other frameworks and languages:&lt;/p&gt;
&lt;div class=&#34;highlight&#34;&gt;&lt;pre tabindex=&#34;0&#34; style=&#34;background-color:#f0f3f3;-moz-tab-size:2;-o-tab-size:2;tab-size:2;&#34;&gt;&lt;code class=&#34;language-python&#34; data-lang=&#34;python&#34;&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;&lt;span style=&#34;color:#99f&#34;&gt;@app.route&lt;/span&gt;(&lt;span style=&#34;color:#c30&#34;&gt;&#39;/webhooks/email&#39;&lt;/span&gt;, methods&lt;span style=&#34;color:#555&#34;&gt;=&lt;/span&gt;[&lt;span style=&#34;color:#c30&#34;&gt;&#39;POST&#39;&lt;/span&gt;])
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;&lt;span style=&#34;color:#069;font-weight:bold&#34;&gt;def&lt;/span&gt; &lt;span style=&#34;color:#c0f&#34;&gt;parse_email&lt;/span&gt;():
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;  to_address &lt;span style=&#34;color:#555&#34;&gt;=&lt;/span&gt; request&lt;span style=&#34;color:#555&#34;&gt;.&lt;/span&gt;form&lt;span style=&#34;color:#555&#34;&gt;.&lt;/span&gt;get(&lt;span style=&#34;color:#c30&#34;&gt;&#39;to&#39;&lt;/span&gt;)
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;  from_address &lt;span style=&#34;color:#555&#34;&gt;=&lt;/span&gt; request&lt;span style=&#34;color:#555&#34;&gt;.&lt;/span&gt;form&lt;span style=&#34;color:#555&#34;&gt;.&lt;/span&gt;get(&lt;span style=&#34;color:#c30&#34;&gt;&#39;from&#39;&lt;/span&gt;)
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;  text &lt;span style=&#34;color:#555&#34;&gt;=&lt;/span&gt; request&lt;span style=&#34;color:#555&#34;&gt;.&lt;/span&gt;form&lt;span style=&#34;color:#555&#34;&gt;.&lt;/span&gt;get(&lt;span style=&#34;color:#c30&#34;&gt;&#39;text&#39;&lt;/span&gt;)
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;  subject &lt;span style=&#34;color:#555&#34;&gt;=&lt;/span&gt; request&lt;span style=&#34;color:#555&#34;&gt;.&lt;/span&gt;form&lt;span style=&#34;color:#555&#34;&gt;.&lt;/span&gt;get(&lt;span style=&#34;color:#c30&#34;&gt;&#39;subject&#39;&lt;/span&gt;)
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;  &lt;span style=&#34;color:#366&#34;&gt;print&lt;/span&gt;(to_address, from_address, subject)
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;  &lt;span style=&#34;color:#069;font-weight:bold&#34;&gt;return&lt;/span&gt; jsonify({&lt;span style=&#34;color:#c30&#34;&gt;&#39;processed&#39;&lt;/span&gt;: &lt;span style=&#34;color:#069;font-weight:bold&#34;&gt;True&lt;/span&gt;})
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;We could then deploy our app, send a couple of emails to addresses the app can receive (e.g. &lt;code&gt;test.1234@mail.example.com&lt;/code&gt;) and view the app’s logs to make sure everything is coming through OK.&lt;/p&gt;
&lt;p&gt;Once we’re happy with the basic setup, we can make changes to the &lt;code&gt;parse_email&lt;/code&gt; function to actually do something useful. For example:&lt;/p&gt;
&lt;div class=&#34;highlight&#34;&gt;&lt;pre tabindex=&#34;0&#34; style=&#34;background-color:#f0f3f3;-moz-tab-size:2;-o-tab-size:2;tab-size:2;&#34;&gt;&lt;code class=&#34;language-python&#34; data-lang=&#34;python&#34;&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;  &lt;span style=&#34;color:#09f;font-style:italic&#34;&gt;# function definition, etc.&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;  email_user &lt;span style=&#34;color:#555&#34;&gt;=&lt;/span&gt; to_address&lt;span style=&#34;color:#555&#34;&gt;.&lt;/span&gt;split(&lt;span style=&#34;color:#c30&#34;&gt;&#39;@&#39;&lt;/span&gt;)[&lt;span style=&#34;color:#f60&#34;&gt;0&lt;/span&gt;]
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;  email_topic &lt;span style=&#34;color:#555&#34;&gt;=&lt;/span&gt; email_user&lt;span style=&#34;color:#555&#34;&gt;.&lt;/span&gt;split(&lt;span style=&#34;color:#c30&#34;&gt;&#39;.&#39;&lt;/span&gt;)[&lt;span style=&#34;color:#f60&#34;&gt;0&lt;/span&gt;] &lt;span style=&#34;color:#09f;font-style:italic&#34;&gt;# `message`&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;  &lt;span style=&#34;color:#069;font-weight:bold&#34;&gt;if&lt;/span&gt; email_topic &lt;span style=&#34;color:#555&#34;&gt;==&lt;/span&gt; &lt;span style=&#34;color:#c30&#34;&gt;&#39;message&#39;&lt;/span&gt;: &lt;span style=&#34;color:#09f;font-style:italic&#34;&gt;# Branch code depending on the type of message&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;    email_topic_id &lt;span style=&#34;color:#555&#34;&gt;=&lt;/span&gt; email_user&lt;span style=&#34;color:#555&#34;&gt;.&lt;/span&gt;split(&lt;span style=&#34;color:#c30&#34;&gt;&#39;.&#39;&lt;/span&gt;)[&lt;span style=&#34;color:#f60&#34;&gt;1&lt;/span&gt;] &lt;span style=&#34;color:#09f;font-style:italic&#34;&gt;# `xxxyyyzzz`&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;    message &lt;span style=&#34;color:#555&#34;&gt;=&lt;/span&gt; db&lt;span style=&#34;color:#555&#34;&gt;.&lt;/span&gt;getMessageWithId(email_topic_id) &lt;span style=&#34;color:#09f;font-style:italic&#34;&gt;# Replace with your own database logic&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;    from_user &lt;span style=&#34;color:#555&#34;&gt;=&lt;/span&gt; db&lt;span style=&#34;color:#555&#34;&gt;.&lt;/span&gt;getUserWithEmail(from_address) &lt;span style=&#34;color:#09f;font-style:italic&#34;&gt;# Get the originator user&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;    &lt;span style=&#34;color:#069;font-weight:bold&#34;&gt;if&lt;/span&gt; &lt;span style=&#34;color:#000;font-weight:bold&#34;&gt;not&lt;/span&gt; message &lt;span style=&#34;color:#000;font-weight:bold&#34;&gt;or&lt;/span&gt; &lt;span style=&#34;color:#000;font-weight:bold&#34;&gt;not&lt;/span&gt; from_user:
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;      &lt;span style=&#34;color:#09f;font-style:italic&#34;&gt;# Handle not found cases&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;    &lt;span style=&#34;color:#069;font-weight:bold&#34;&gt;if&lt;/span&gt; from_user&lt;span style=&#34;color:#555&#34;&gt;.&lt;/span&gt;id &lt;span style=&#34;color:#000;font-weight:bold&#34;&gt;not&lt;/span&gt; &lt;span style=&#34;color:#000;font-weight:bold&#34;&gt;in&lt;/span&gt; message&lt;span style=&#34;color:#555&#34;&gt;.&lt;/span&gt;participants:
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;      &lt;span style=&#34;color:#09f;font-style:italic&#34;&gt;# Handle case where the user isn&#39;t allowed to reply to the message&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;    db&lt;span style=&#34;color:#555&#34;&gt;.&lt;/span&gt;createNewMessage(from_user&lt;span style=&#34;color:#555&#34;&gt;.&lt;/span&gt;id, text, in_reply_to &lt;span style=&#34;color:#555&#34;&gt;=&lt;/span&gt; message&lt;span style=&#34;color:#555&#34;&gt;.&lt;/span&gt;id) &lt;span style=&#34;color:#09f;font-style:italic&#34;&gt;# Create the message&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;  &lt;span style=&#34;color:#069;font-weight:bold&#34;&gt;return&lt;/span&gt; jsonify({&lt;span style=&#34;color:#c30&#34;&gt;&#39;processed&#39;&lt;/span&gt;: &lt;span style=&#34;color:#069;font-weight:bold&#34;&gt;True&lt;/span&gt;})
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;In this example we have included some checks to ensure the user exists and has the permission needed to reply to the message. We parse the relevant message details from the &lt;code&gt;From&lt;/code&gt; field as discussed in the previous section.&lt;/p&gt;
&lt;h2 id=&#34;conclusion&#34;&gt;Conclusion&lt;/h2&gt;
&lt;p&gt;We’ve now run through the complete end-to-end set-up; users can now click “reply” to emails in order to send content back through to our platform.&lt;/p&gt;
&lt;p&gt;Of course, this approach isn’t perfect. For example, changes would be needed to handle the following:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;Newly-created messages may include quoted content in the email that we’d need to remove (and things like signatures).&lt;/li&gt;
&lt;li&gt;We should add extra text to our outbound email telling users that they can reply to it.&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;However, hopefully this post has set the scene for how one might go about implementing such a system in their own software!&lt;/p&gt;
</content:encoded>
    </item>
    
    <item>
      <title>Open-sourcing projects</title>
      <link>https://wilw.dev/blog/2021/12/02/open-sourcing/</link>
      <pubDate>Thu, 02 Dec 2021 16:01:00 +0000</pubDate>
      
      <guid>https://wilw.dev/blog/2021/12/02/open-sourcing/</guid>
      
        <category>100daystooffload</category>
      
        <category>technology</category>
      
        <category>life</category>
      
      
      <content:encoded>&lt;p&gt;I maintain a small number of &lt;a href=&#34;https://wilw.dev/projects&#34;&gt;projects&lt;/a&gt; in my spare time. The amount of time I get to work on and maintain these varies depending on my other workloads.&lt;/p&gt;
&lt;p&gt;The projects were never designed to be a means of making additional income, and were usually created simply to solve a need that I (or somebody else I know) had!&lt;/p&gt;
&lt;p&gt;By open-sourcing them I hope that others will read through the code, and even check for (and report) problems or potentially contribute. The projects are licensed quite liberally under the BSD license.&lt;/p&gt;
&lt;p&gt;The following projects are affected by this change:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;a href=&#34;https://treadl.com&#34; target=&#34;_blank&#34; rel=&#34;noopener noreferrer&#34; &gt;Treadl&lt;/a&gt; (👨‍💻 &lt;a href=&#34;https://git.wilw.dev/seastorm/treadl&#34; target=&#34;_blank&#34; rel=&#34;noopener noreferrer&#34; &gt;source code&lt;/a&gt;)&lt;/li&gt;
&lt;li&gt;&lt;a href=&#34;https://sso.tools&#34; target=&#34;_blank&#34; rel=&#34;noopener noreferrer&#34; &gt;SSO Tools&lt;/a&gt; (👨‍💻 &lt;a href=&#34;https://git.wilw.dev/seastorm/sso-tools&#34; target=&#34;_blank&#34; rel=&#34;noopener noreferrer&#34; &gt;source code&lt;/a&gt;)&lt;/li&gt;
&lt;li&gt;&lt;a href=&#34;gemini://capsule.town&#34;&gt;Capsule Town&lt;/a&gt; (🌍 &lt;a href=&#34;https://portal.mozz.us/gemini/capsule.town&#34; target=&#34;_blank&#34; rel=&#34;noopener noreferrer&#34; &gt;via web portal&lt;/a&gt;) (👨‍💻 &lt;a href=&#34;https://git.wilw.dev/wilw/capsule-town&#34; target=&#34;_blank&#34; rel=&#34;noopener noreferrer&#34; &gt;source code&lt;/a&gt;)&lt;/li&gt;
&lt;li&gt;&lt;a href=&#34;https://dotty.cloud&#34; target=&#34;_blank&#34; rel=&#34;noopener noreferrer&#34; &gt;Dotty&lt;/a&gt; (👨‍💻 &lt;a href=&#34;https://git.wilw.dev/seastorm/dotty&#34; target=&#34;_blank&#34; rel=&#34;noopener noreferrer&#34; &gt;source code&lt;/a&gt;)&lt;/li&gt;
&lt;/ul&gt;
</content:encoded>
    </item>
    
    <item>
      <title>Server back-ups with Restic</title>
      <link>https://wilw.dev/blog/2021/11/27/restic-backups/</link>
      <pubDate>Sat, 27 Nov 2021 10:20:00 +0000</pubDate>
      
      <guid>https://wilw.dev/blog/2021/11/27/restic-backups/</guid>
      
        <category>100daystooffload</category>
      
        <category>technology</category>
      
        <category>selfhost</category>
      
      
      <content:encoded>&lt;p&gt;A while ago I &lt;a href=&#34;https://wilw.dev/blog/2021/05/18/b2-backups&#34;&gt;posted about&lt;/a&gt; how I back-up my personal servers to &lt;a href=&#34;https://www.backblaze.com/cloud-storage&#34; target=&#34;_blank&#34; rel=&#34;noopener noreferrer&#34; &gt;Backblaze B2&lt;/a&gt;. That approach involved adding all of the files and directories into a single compressed archive and sending this up to an encrypted bucket on B2.&lt;/p&gt;
&lt;p&gt;Whilst this does achieve my back-up goals (and I use &lt;a href=&#34;https://wilw.dev/blog/2021/09/09/telegram-notifications&#34;&gt;Telegram to notify me each time it completes&lt;/a&gt;), it felt &lt;em&gt;inelegant&lt;/em&gt;. Every time the back-up executed, the entirety of the back-up file - several gigabytes - would be built and transferred.&lt;/p&gt;
&lt;p&gt;In this post I will talk about my new approach, which uses a tool called Restic.&lt;/p&gt;
&lt;h2 id=&#34;restic-repositories-and-backblaze-b2&#34;&gt;Restic, repositories, and Backblaze B2&lt;/h2&gt;
&lt;p&gt;&lt;a href=&#34;https://restic.net&#34; target=&#34;_blank&#34; rel=&#34;noopener noreferrer&#34; &gt;Restic&lt;/a&gt; is an open-source cross-platform program to back-up files and directories. It works on the basis of &lt;a href=&#34;https://restic.readthedocs.io/en/stable/045_working_with_repos.html&#34; target=&#34;_blank&#34; rel=&#34;noopener noreferrer&#34; &gt;snapshots and repositories&lt;/a&gt;.&lt;/p&gt;
&lt;p&gt;A single repository can contain multiple “snapshots” representing various back-ups. Each snapshot is timestamped and can be filtered by the path of the back-up and the host that the back-up came from.&lt;/p&gt;
&lt;p&gt;One of the most useful features of Restic is that each snapshot only stores the representation of what &lt;em&gt;changed&lt;/em&gt; since the last snapshot. This means that although the first back-up might take a little while, subsequent snapshots are much quicker.&lt;/p&gt;
&lt;p&gt;Repositories can be local (i.e. on a filesystem accessible by the host), or remote. Restic &lt;a href=&#34;https://restic.readthedocs.io/en/stable/030_preparing_a_new_repo.html&#34; target=&#34;_blank&#34; rel=&#34;noopener noreferrer&#34; &gt;supports a number of remote repository types&lt;/a&gt; - from SFTP and RESTful interfaces through to S3-compatible backends and storage services operated by cloud providers.&lt;/p&gt;
&lt;p&gt;Restic also supports Backblaze B2 out of the box, which is the repository type that I use.&lt;/p&gt;
&lt;p&gt;In the rest of this post I will run through how this can be set-up and automated.&lt;/p&gt;
&lt;h2 id=&#34;preparing-a-restic-repository-on-b2&#34;&gt;Preparing a Restic repository on B2&lt;/h2&gt;
&lt;p&gt;First, we need to install Restic. There are instructions for installation on a range of systems on &lt;a href=&#34;https://restic.readthedocs.io/en/stable/020_installation.html&#34; target=&#34;_blank&#34; rel=&#34;noopener noreferrer&#34; &gt;the Restic website&lt;/a&gt;.&lt;/p&gt;
&lt;p&gt;Next, in the Backblaze B2 console, create a new bucket. The bucket will need a unique name. Whilst there, create a new “App Key”, with read/write access to the bucket. Make a note of the &lt;code&gt;keyID&lt;/code&gt; and &lt;code&gt;applicationKey&lt;/code&gt; once created.&lt;/p&gt;
&lt;p&gt;You can close the B2 console. You now need a file that can be &lt;code&gt;source&lt;/code&gt;d to tell Restic how to access your repository. Create a new file (e.g. in the home directory of the host you want to back-up), called &lt;code&gt;resticenv&lt;/code&gt;, with these contents:&lt;/p&gt;
&lt;div class=&#34;highlight&#34;&gt;&lt;pre tabindex=&#34;0&#34; style=&#34;background-color:#f0f3f3;-moz-tab-size:2;-o-tab-size:2;tab-size:2;&#34;&gt;&lt;code class=&#34;language-env&#34; data-lang=&#34;env&#34;&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;&lt;span style=&#34;color:#366&#34;&gt;export&lt;/span&gt; &lt;span style=&#34;color:#033&#34;&gt;B2_ACCOUNT_ID&lt;/span&gt;&lt;span style=&#34;color:#555&#34;&gt;=&lt;/span&gt;&lt;span style=&#34;color:#c30&#34;&gt;&#34;&lt;KEY_ID&gt;&#34;&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;&lt;span style=&#34;color:#366&#34;&gt;export&lt;/span&gt; &lt;span style=&#34;color:#033&#34;&gt;B2_ACCOUNT_KEY&lt;/span&gt;&lt;span style=&#34;color:#555&#34;&gt;=&lt;/span&gt;&lt;span style=&#34;color:#c30&#34;&gt;&#34;&lt;APPLICATION_KEY&gt;&#34;&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;&lt;span style=&#34;color:#366&#34;&gt;export&lt;/span&gt; &lt;span style=&#34;color:#033&#34;&gt;RESTIC_REPOSITORY&lt;/span&gt;&lt;span style=&#34;color:#555&#34;&gt;=&lt;/span&gt;&lt;span style=&#34;color:#c30&#34;&gt;&#34;b2:&lt;BUCKET_NAME&gt;&#34;&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;&lt;span style=&#34;color:#366&#34;&gt;export&lt;/span&gt; &lt;span style=&#34;color:#033&#34;&gt;RESTIC_PASSWORD&lt;/span&gt;&lt;span style=&#34;color:#555&#34;&gt;=&lt;/span&gt;&lt;span style=&#34;color:#c30&#34;&gt;&#34;&lt;PASSWORD&gt;&#34;&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;You’ll need to replace some of the parts of the file as described below:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;code&gt;&lt;KEY_ID&gt;&lt;/code&gt;: the &lt;code&gt;keyID&lt;/code&gt; of the B2 key you created earlier&lt;/li&gt;
&lt;li&gt;&lt;code&gt;&lt;APPLICATION_KEY&gt;&lt;/code&gt;: the &lt;code&gt;applicationKey&lt;/code&gt; of the B2 key you created earlier&lt;/li&gt;
&lt;li&gt;&lt;code&gt;&lt;BUCKET_NAME&gt;&lt;/code&gt;: the name of the B2 bucket you created earlier&lt;/li&gt;
&lt;li&gt;&lt;code&gt;&lt;PASSWORD&gt;&lt;/code&gt;: a strong and private password&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;Each Restic repository is secured with a &lt;em&gt;password&lt;/em&gt;. This is used to encrypt the repository, so if this is lost then so is the associated data. I use my password manager to generate a strong password. It goes without saying that this &lt;code&gt;resticenv&lt;/code&gt; file should be kept very safe.&lt;/p&gt;
&lt;p&gt;Finally, we need to initialise the repository. To do so, first &lt;code&gt;source&lt;/code&gt; the environment file, and then use the &lt;code&gt;init&lt;/code&gt; command:&lt;/p&gt;
&lt;div class=&#34;highlight&#34;&gt;&lt;pre tabindex=&#34;0&#34; style=&#34;background-color:#f0f3f3;-moz-tab-size:2;-o-tab-size:2;tab-size:2;&#34;&gt;&lt;code class=&#34;language-shell&#34; data-lang=&#34;shell&#34;&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;$ &lt;span style=&#34;color:#366&#34;&gt;source&lt;/span&gt; ~/resticenv
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;$ restic init
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;You’re now ready to create back-ups.&lt;/p&gt;
&lt;h2 id=&#34;creating-a-new-back-up&#34;&gt;Creating a new back-up&lt;/h2&gt;
&lt;p&gt;Creating a new back-up is as easy as running the following (as long as your shell still has the &lt;code&gt;source&lt;/code&gt;d environment as described above):&lt;/p&gt;
&lt;div class=&#34;highlight&#34;&gt;&lt;pre tabindex=&#34;0&#34; style=&#34;background-color:#f0f3f3;-moz-tab-size:2;-o-tab-size:2;tab-size:2;&#34;&gt;&lt;code class=&#34;language-shell&#34; data-lang=&#34;shell&#34;&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;$ restic backup /path/to/directory
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;If the target directory is particularly large then the first back-up may take a while. However, subsequent back-ups will be much quicker.&lt;/p&gt;
&lt;p&gt;Once done, you can run the following to view a list of all the snapshots held by the repository. You’ll see your recently-created back-up listed:&lt;/p&gt;
&lt;div class=&#34;highlight&#34;&gt;&lt;pre tabindex=&#34;0&#34; style=&#34;background-color:#f0f3f3;-moz-tab-size:2;-o-tab-size:2;tab-size:2;&#34;&gt;&lt;code class=&#34;language-shell&#34; data-lang=&#34;shell&#34;&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;$ restic snapshots
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;ID        Time                 Host        Tags      Paths
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;-----------------------------------------------------------------------
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;6b25863c  2021-11-27 08:39:44  myhost                /path/to/directory
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;-----------------------------------------------------------------------
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;&lt;span style=&#34;color:#f60&#34;&gt;1&lt;/span&gt; snapshot
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;The next time you run &lt;code&gt;restic backup&lt;/code&gt; again, you’ll notice new snapshots are created.&lt;/p&gt;
&lt;h2 id=&#34;pruning-snapshots&#34;&gt;Pruning snapshots&lt;/h2&gt;
&lt;p&gt;In some scenarios you may not want to keep back-ups around forever. For example, if you provide a service to users you may be in breach of your own Privacy Policy if you keep backed-up data of old deleted accounts.&lt;/p&gt;
&lt;p&gt;Luckily it’s straight forward to “forget” old snapshots. The command below tells Restic to hold onto the last 24 hours’ worth of hourly snapshots and the last 7 days’ worth of daily snapshots:&lt;/p&gt;
&lt;div class=&#34;highlight&#34;&gt;&lt;pre tabindex=&#34;0&#34; style=&#34;background-color:#f0f3f3;-moz-tab-size:2;-o-tab-size:2;tab-size:2;&#34;&gt;&lt;code class=&#34;language-shell&#34; data-lang=&#34;shell&#34;&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;$ restic forget --prune --keep-hourly &lt;span style=&#34;color:#f60&#34;&gt;24&lt;/span&gt; --keep-daily &lt;span style=&#34;color:#f60&#34;&gt;7&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;h2 id=&#34;recovering-snapshots&#34;&gt;Recovering snapshots&lt;/h2&gt;
&lt;p&gt;Back-ups are useless unless you can later recover them! Rustic makes this easy. You just need the &lt;code&gt;ID&lt;/code&gt; of the back-up you want to recover (as provided by the &lt;code&gt;restic snapshots&lt;/code&gt; command from earlier):&lt;/p&gt;
&lt;div class=&#34;highlight&#34;&gt;&lt;pre tabindex=&#34;0&#34; style=&#34;background-color:#f0f3f3;-moz-tab-size:2;-o-tab-size:2;tab-size:2;&#34;&gt;&lt;code class=&#34;language-shell&#34; data-lang=&#34;shell&#34;&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;$ restic restore 6b25863c --target ~/restored
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;The &lt;code&gt;~/restored&lt;/code&gt; directory should now contain the files included in the back-up at that snapshot’s point in time.&lt;/p&gt;
&lt;h2 id=&#34;automating-back-ups&#34;&gt;Automating back-ups&lt;/h2&gt;
&lt;p&gt;Finally we can cover a way to automate back-ups. For this we can simply use &lt;code&gt;cron&lt;/code&gt;. To begin, install a suitable cron service (if your system doesn’t come with one). I use &lt;a href=&#34;https://github.com/cronie-crond/cronie&#34; target=&#34;_blank&#34; rel=&#34;noopener noreferrer&#34; &gt;cronie&lt;/a&gt;.&lt;/p&gt;
&lt;p&gt;Once ready, add an entry to your user’s crontab by running &lt;code&gt;crontab -e&lt;/code&gt; and adding the following line:&lt;/p&gt;
&lt;pre tabindex=&#34;0&#34;&gt;&lt;code&gt;15 * * * * . /home/&lt;USER&gt;/resticenv; /usr/bin/restic backup -q /path/to/directory
&lt;/code&gt;&lt;/pre&gt;&lt;p&gt;The above job will run at quarter-past the hour for every hour the system is running. Note that cron requires that we include full paths to files. The &lt;code&gt;-q&lt;/code&gt; flag suppresses output. You’ll need to change parts of the line to suit your needs (run &lt;code&gt;which restic&lt;/code&gt; to see where Restic is installed on your system).&lt;/p&gt;
&lt;h2 id=&#34;conclusion&#34;&gt;Conclusion&lt;/h2&gt;
&lt;p&gt;I have much more confidence in a tool like Restic to handle my back-ups than a home-grown solution. It’s great to have the peace of mind!&lt;/p&gt;
</content:encoded>
    </item>
    
    <item>
      <title>Nightfall City</title>
      <link>https://wilw.dev/blog/2021/11/25/nightfall-city/</link>
      <pubDate>Thu, 25 Nov 2021 18:36:00 +0000</pubDate>
      
      <guid>https://wilw.dev/blog/2021/11/25/nightfall-city/</guid>
      
        <category>100daystooffload</category>
      
        <category>technology</category>
      
        <category>life</category>
      
      
      <content:encoded>&lt;blockquote&gt;
&lt;p&gt;From the hills of Dusk’s End to the small alleys of Main Street, you feel drawn to the lights of this vibrant metropolis in an uncharted internet territory. The sign reads “Nightfall”.&lt;/p&gt;
&lt;p&gt;– &lt;em&gt;Nightfall City&lt;/em&gt;&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;The &lt;a href=&#34;gemini://nightfall.city&#34;&gt;Nightfall City&lt;/a&gt; Gemini capsule (also available &lt;a href=&#34;https://nightfall.city&#34; target=&#34;_blank&#34; rel=&#34;noopener noreferrer&#34; &gt;via the web&lt;/a&gt;) is an internet community in which people can engage with each other and write blog posts and other long-form content.&lt;/p&gt;
&lt;p&gt;The community is divided into different &lt;em&gt;districts&lt;/em&gt; of Nightfall City: &lt;a href=&#34;https://nightfall.city/main-street&#34; target=&#34;_blank&#34; rel=&#34;noopener noreferrer&#34; &gt;Main Street&lt;/a&gt;, &lt;a href=&#34;https://nightfall.city/dusks-end&#34; target=&#34;_blank&#34; rel=&#34;noopener noreferrer&#34; &gt;Dusk’s End&lt;/a&gt;, and &lt;a href=&#34;https://nightfall.city/writers-lane&#34; target=&#34;_blank&#34; rel=&#34;noopener noreferrer&#34; &gt;Writer’s Lane&lt;/a&gt;. These districts allow members (or “citizens”) to post links to their blog posts, zines, or other online articles. From what I can see, people tend to participate in the district or community that best fits them as an individual.&lt;/p&gt;
&lt;p&gt;There is also the &lt;a href=&#34;https://midnight.pub&#34; target=&#34;_blank&#34; rel=&#34;noopener noreferrer&#34; &gt;Midnight Pub&lt;/a&gt; - in a small alley just off Main Street. Here one can write posts and thoughts directly.&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;It’s late. You are seconds away from the main street in a small alley. It’s quieter here, but you can still hear the sound of chatter, footsteps, and cars from busy downtown. The city is buzzing, the streets are like arteries. You see an intriguing place in the alley, with a moon on its door. It reads “The Midnight Pub”.&lt;/p&gt;
&lt;p&gt;– &lt;em&gt;The Midnight Pub&lt;/em&gt;&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;I won’t say much more about Nightfall City, or its various districts, as I can recommend taking some time to explore through yourself. I enjoy coming back every now and again to read some of the recent posts. It’s another of those great - yet quirky - corners of the internet.&lt;/p&gt;
</content:encoded>
    </item>
    
    <item>
      <title>Webzines</title>
      <link>https://wilw.dev/blog/2021/11/20/webzines/</link>
      <pubDate>Sat, 20 Nov 2021 14:40:00 +0000</pubDate>
      
      <guid>https://wilw.dev/blog/2021/11/20/webzines/</guid>
      
        <category>100daystooffload</category>
      
        <category>technology</category>
      
        <category>life</category>
      
      
      <content:encoded>&lt;p&gt;I’ve really enjoyed my recent discovery of a couple of traditional-style &lt;em&gt;webzines&lt;/em&gt;. Webzines (sometimes referred to as &lt;a href=&#34;https://en.wikipedia.org/wiki/Online_magazine&#34; target=&#34;_blank&#34; rel=&#34;noopener noreferrer&#34; &gt;online magazines&lt;/a&gt;, or - in this instance - simply “zines”) are a way of distributing periodic content through the web.&lt;/p&gt;
&lt;p&gt;I’m not referring to modern-day online media outlets, but those publications which are typically written by a small number of individuals (often “netizens”) and where the focus is not on advertisements, clickbait, or the mass production of content.&lt;/p&gt;
&lt;p&gt;Such outputs differ from blogs, in the sense that they often feature contributions from a number of people and sources, and potentially exhibit some form of editorial process.&lt;/p&gt;
&lt;p&gt;They can be used to distribute news, updates, opinion, or anything else that you might find in any other type of periodical.&lt;/p&gt;
&lt;p&gt;I like them because they reflect the values of the &lt;a href=&#34;https://jackcheng.com/essays/the-slow-web&#34; target=&#34;_blank&#34; rel=&#34;noopener noreferrer&#34; &gt;slow web&lt;/a&gt;, they’re informative, can be quite informal and quirky, quick and easy to consume, and they help with discovering new and interesting content from around the web.&lt;/p&gt;
&lt;p&gt;I’ve only discovered a few examples so far - such as the &lt;a href=&#34;https://webzine.puffy.cafe&#34; target=&#34;_blank&#34; rel=&#34;noopener noreferrer&#34; &gt;OpenBSD Webzine&lt;/a&gt; and the &lt;a href=&#34;https://yesterweb.org/zine&#34; target=&#34;_blank&#34; rel=&#34;noopener noreferrer&#34; &gt;Yesterweb Zine&lt;/a&gt; - but I look forward to staying on the look-out for more.&lt;/p&gt;
</content:encoded>
    </item>
    
    <item>
      <title>Rebel Ideas by Matthew Syed</title>
      <link>https://wilw.dev/blog/2021/11/17/rebel-ideas/</link>
      <pubDate>Wed, 17 Nov 2021 12:56:00 +0000</pubDate>
      
      <guid>https://wilw.dev/blog/2021/11/17/rebel-ideas/</guid>
      
        <category>100daystooffload</category>
      
        <category>book</category>
      
      
      <content:encoded>&lt;p&gt;I feel that this book really resonated with my own thoughts around the importance of diversity in groups and teams.&lt;/p&gt;
&lt;p&gt;&lt;img src=&#34;https://wilw.dev/media/blog/rebel_ideas.jpg&#34; alt=&#34;The Rebel Ideas book cover&#34;&gt;&lt;/p&gt;
&lt;p&gt;&lt;a href=&#34;https://www.goodreads.com/author/show/3414480.Matthew_Syed&#34; target=&#34;_blank&#34; rel=&#34;noopener noreferrer&#34; &gt;Matthew Syed&lt;/a&gt;’s &lt;a href=&#34;https://www.goodreads.com/book/show/52326253-rebel-ideas&#34; target=&#34;_blank&#34; rel=&#34;noopener noreferrer&#34; &gt;Rebel Ideas: The Power of Diverse Thinking&lt;/a&gt; is a book that examines how effectiveness and output can be dramatically altered through building teams that contain &lt;em&gt;diverse thinkers&lt;/em&gt;.&lt;/p&gt;
&lt;p&gt;The book considers a number of examples - from both the past and present - and compares and contrasts scenarios where teams expressing different proportions of diverse thinkers can change their performance; sometimes with life-threatening consequences.&lt;/p&gt;
&lt;p&gt;One of the most interesting concepts discussed by the author is that around &lt;em&gt;problem spaces&lt;/em&gt;. When solving challenges, there is a cognitive space that must be considered and approached in order to produce a more effective outcome. By recruiting diverse thinkers, there is less overlap in each individuals’ own representation of the space, and therefore more of the space can be explored.&lt;/p&gt;
&lt;p&gt;Overlap in the space can even be dentrimental to the performance of the group. If there is sufficient overlap, then those members will simply reaffirm each others’ thoughts and ideas to the point where these opinions become dominant and drown out the inputs from members occupying other areas of the space.&lt;/p&gt;
&lt;p&gt;In relation to this, the book considers the example of a sprint relay team. In this type of scenario, you’d want to involve the four fastest sprinters you can find - no matter their background - in order to win the race. However, in &lt;em&gt;cognitive&lt;/em&gt; challenges - for example, to accurately create financial forecasts - teams consisting of a number of people from different financial backgrounds, models, and ways of thinking, out-performed the individuals known to be “the best in the game”.&lt;/p&gt;
&lt;p&gt;Interestingly, the author also analyses how &lt;em&gt;cultural&lt;/em&gt; diversity can (but doesn’t always) impact &lt;em&gt;cognitive&lt;/em&gt; diversity. In many scenarios, people from different cultural backgrounds may develop their own ways of thinking through their families, social groups, and education.&lt;/p&gt;
&lt;p&gt;On the other side of this, two people from different cultural backgrounds, but who attended the same university and were tought the same models by the same lecturers, are likely to inhabit the same area of the problem space, and thus overlap in their ability and knowledge.&lt;/p&gt;
&lt;p&gt;There are many more examples considered by the book, and I can strongly recommend reading it - particularly if you manage or frequently work in a team yourself. The general upshot is that the &lt;em&gt;rebel&lt;/em&gt; ideas - i.e. those ideas that are truly innovative and ground breaking - are more likely to emerge from teams of people that &lt;em&gt;think different&lt;/em&gt;.&lt;/p&gt;
</content:encoded>
    </item>
    
    <item>
      <title>On websites and creativity</title>
      <link>https://wilw.dev/blog/2021/11/13/website-creativity/</link>
      <pubDate>Sat, 13 Nov 2021 11:13:00 +0000</pubDate>
      
      <guid>https://wilw.dev/blog/2021/11/13/website-creativity/</guid>
      
        <category>100daystooffload</category>
      
        <category>technology</category>
      
        <category>opinion</category>
      
      
      <content:encoded>&lt;p&gt;I’ve recently been reminiscing about the “old” days of the web. They felt much more like expressions of personality and creativity.&lt;/p&gt;
&lt;p&gt;These days, most people have social media accounts on mainstream services that act as their sole representation of themselves online. Whilst the &lt;em&gt;content&lt;/em&gt; can be different, everyone’s own pages end up looking the same, with avatar images, feeds, and other components having layouts and “look and feel&#34;s controlled by the service - the creativity is lost and things become bland.&lt;/p&gt;
&lt;p&gt;This looks like the current trend in such platforms. Previously, MySpace offered some nice ways to customise things on individuals’ pages, but before that the main way to fully control your representation online was to &lt;em&gt;own&lt;/em&gt; your space - i.e. have an actual website you can control.&lt;/p&gt;
&lt;p&gt;Personal websites are still very much a thing, and in many ways are making a bit of a comeback. The concepts are described nicely in &lt;a href=&#34;https://ar.al/2020/08/07/what-is-the-small-web&#34; target=&#34;_blank&#34; rel=&#34;noopener noreferrer&#34; &gt;Aral Balkan’s blog&lt;/a&gt;, in which there are also arguments that personal websites can interconnect - an initiave also made possible through technologies like &lt;a href=&#34;https://www.w3.org/TR/activitypub&#34; target=&#34;_blank&#34; rel=&#34;noopener noreferrer&#34; &gt;ActivityPub&lt;/a&gt;.&lt;/p&gt;
&lt;p&gt;Either way, modern personal websites tend to all be quite minimalistic and sleek. People often share tools and software (e.g. CSS libraries), which are good ways to get something off the ground or to help enable those that are less tech-able. Whilst this is also great for providing useful reading experiences, enhancing accessibility, and encouraging mobile-first principles, I worry that perhaps something is lost in the expression of creativity.&lt;/p&gt;
&lt;p&gt;I recently stumbled across &lt;a href=&#34;https://yesterweb.org/webring/members.html&#34; target=&#34;_blank&#34; rel=&#34;noopener noreferrer&#34; &gt;the Yesterweb webring&lt;/a&gt;. For those not familiar with the concept, by joining your site to a webring it gives your website additional exposure, since each member of the ring publishes a link to the next site along. As a visitor, they’re a great way to explore and discover new sites.&lt;/p&gt;
&lt;p&gt;This particular webring showcases websites with very distinctive 90s vibes - many of which are hosted on &lt;a href=&#34;https://neocities.org&#34; target=&#34;_blank&#34; rel=&#34;noopener noreferrer&#34; &gt;Neocities&lt;/a&gt;. They feature bright colours, lots of scrolling banners, animated GIFs and more. Some also bring back concepts like visitor counts, guestbooks and chatboxes. It’s a refreshing and fun experience.&lt;/p&gt;
&lt;p&gt;It’s great to see and to explore! I will show and link to a few below, but I recommend &lt;a href=&#34;https://yesterweb.org/webring/members.html&#34; target=&#34;_blank&#34; rel=&#34;noopener noreferrer&#34; &gt;taking a look through yourself&lt;/a&gt; too.&lt;/p&gt;
&lt;h3 id=&#34;sadgrlonlinehttpssadgrlonline&#34;&gt;&lt;a href=&#34;https://sadgrl.online&#34; target=&#34;_blank&#34; rel=&#34;noopener noreferrer&#34; &gt;sadgrl.online&lt;/a&gt;&lt;/h3&gt;
&lt;p&gt;&lt;img src=&#34;https://wilw.dev/media/blog/sadgirl.png&#34; alt=&#34;sadgrl.online website screenshot&#34;&gt;&lt;/p&gt;
&lt;h3 id=&#34;bytemothneocitiesorghttpsbytemothneocitiesorg&#34;&gt;&lt;a href=&#34;https://bytemoth.neocities.org&#34; target=&#34;_blank&#34; rel=&#34;noopener noreferrer&#34; &gt;bytemoth.neocities.org&lt;/a&gt;&lt;/h3&gt;
&lt;p&gt;&lt;img src=&#34;https://wilw.dev/media/blog/bytemoth.png&#34; alt=&#34;bytemoth.neocities.org website screenshot&#34;&gt;&lt;/p&gt;
&lt;h3 id=&#34;cinninethttpscinninet&#34;&gt;&lt;a href=&#34;https://cinni.net&#34; target=&#34;_blank&#34; rel=&#34;noopener noreferrer&#34; &gt;cinni.net&lt;/a&gt;&lt;/h3&gt;
&lt;p&gt;&lt;img src=&#34;https://wilw.dev/media/blog/cinninet.png&#34; alt=&#34;cinni.net website screenshot&#34;&gt;&lt;/p&gt;
&lt;h3 id=&#34;auzziejaycomhttpsauzziejaycom&#34;&gt;&lt;a href=&#34;https://auzziejay.com&#34; target=&#34;_blank&#34; rel=&#34;noopener noreferrer&#34; &gt;auzziejay.com&lt;/a&gt;&lt;/h3&gt;
&lt;p&gt;&lt;img src=&#34;https://wilw.dev/media/blog/auzziejay.png&#34; alt=&#34;auzziejay.com website screenshot&#34;&gt;&lt;/p&gt;
&lt;h3 id=&#34;haxrelmneocitiesorghttpshaxrelmneocitiesorg&#34;&gt;&lt;a href=&#34;https://haxrelm.neocities.org&#34; target=&#34;_blank&#34; rel=&#34;noopener noreferrer&#34; &gt;haxrelm.neocities.org&lt;/a&gt;&lt;/h3&gt;
&lt;p&gt;&lt;img src=&#34;https://wilw.dev/media/blog/haxrealm.png&#34; alt=&#34;haxrelm.neocities.org website screenshot&#34;&gt;&lt;/p&gt;
</content:encoded>
    </item>
    
    <item>
      <title>Our DIY bathroom project</title>
      <link>https://wilw.dev/blog/2021/11/10/bathroom-diy/</link>
      <pubDate>Wed, 10 Nov 2021 21:04:00 +0000</pubDate>
      
      <guid>https://wilw.dev/blog/2021/11/10/bathroom-diy/</guid>
      
        <category>100daystooffload</category>
      
        <category>life</category>
      
      
      <content:encoded>&lt;p&gt;I’ve recently posted about our home, in which we’ve completed a few DIY projects, such as &lt;a href=&#34;https://wilw.dev/blog/2021/08/14/garden-renovation&#34;&gt;renovating the garden&lt;/a&gt; and &lt;a href=&#34;https://wilw.dev/blog/2021/10/24/loft-conversion&#34;&gt;building a small loft conversion&lt;/a&gt; (amongst other things!). Today I’m writing about a project we did on the bathroom in the house.&lt;/p&gt;
&lt;p&gt;When we first moved into the house, which was an old student dwelling in disrepair, the first floor had a stange configuration containing a separate toilet and shower room. Both of these rooms were tiny, old, and damp. There was no space for a bath and also no window in the shower room and the extractor fan didn’t work.&lt;/p&gt;
&lt;p&gt;In this post (complete with images) I will walk through the changes we made in order to turn this into a more liveable bathroom. We are by no means experts, but we managed to do the project pretty much by ourselves, with a fair bit of Googling and &lt;em&gt;only&lt;/em&gt; one plumbing mishap!&lt;/p&gt;
&lt;h2 id=&#34;initial-state&#34;&gt;Initial state&lt;/h2&gt;
&lt;p&gt;These images were taken from when we first viewed the house and also the first day we moved in. You can see the grotty toilet, shower, and the small, gloomy rooms.&lt;/p&gt;
&lt;p&gt;&lt;img src=&#34;https://wilw.dev/media/blog/bathroom1.jpg&#34; alt=&#34;The original toilet room, taken from the doorway&#34;&gt;
&lt;img src=&#34;https://wilw.dev/media/blog/bathroom2.jpg&#34; alt=&#34;The tiny sink in the original toilet room&#34;&gt;
&lt;img src=&#34;https://wilw.dev/media/blog/bathroom3.jpg&#34; alt=&#34;The original toilet room, taken from the doorway&#34;&gt;
&lt;img src=&#34;https://wilw.dev/media/blog/bathroom4.jpg&#34; alt=&#34;The original gloomy shower room, taken from the doorway&#34;&gt;
&lt;img src=&#34;https://wilw.dev/media/blog/bathroom5.jpg&#34; alt=&#34;The original shower room&#34;&gt;&lt;/p&gt;
&lt;h2 id=&#34;knocking-down-the-walls&#34;&gt;Knocking down the walls&lt;/h2&gt;
&lt;p&gt;One of the aims of the project was to create one single room containing a toilet, shower, bath, and sink. This meant that the first thing for us to do was to join the existing toilet and shower rooms together, as well as to take part of the bedroom next door.&lt;/p&gt;
&lt;p&gt;Luckily these were all stud walls, so aside from the mess, this wasn’t too much of an issue given a bit of muscle and a sledgehammer.&lt;/p&gt;
&lt;p&gt;In the image below you can see where the previous toilet and shower rooms were. And the remains of where the two walls were. We took an extra window from the bedroom too.&lt;/p&gt;
&lt;p&gt;&lt;img src=&#34;https://wilw.dev/media/blog/bathroom6.jpg&#34; alt=&#34;The mess after knocking the two walls down&#34;&gt;&lt;/p&gt;
&lt;h2 id=&#34;sorting-out-the-plumbing&#34;&gt;Sorting out the plumbing&lt;/h2&gt;
&lt;p&gt;To knock the walls down we needed to turn the mains water off to the house in case we damaged any pipes.&lt;/p&gt;
&lt;p&gt;We then needed to remove the shower fittings and tray properly, and correctly cap all exposed water inlet pipes.&lt;/p&gt;
&lt;p&gt;&lt;img src=&#34;https://wilw.dev/media/blog/bathroom7.jpg&#34; alt=&#34;Capping the shower inlet pipe&#34;&gt;
&lt;img src=&#34;https://wilw.dev/media/blog/bathroom8.jpg&#34; alt=&#34;After removing the old shower tray&#34;&gt;
&lt;img src=&#34;https://wilw.dev/media/blog/bathroom9.jpg&#34; alt=&#34;After removing the old shower tray&#34;&gt;&lt;/p&gt;
&lt;h2 id=&#34;building-the-new-wall-structure&#34;&gt;Building the new wall structure&lt;/h2&gt;
&lt;p&gt;We needed a wall between the new bathroom space and the bedroom (for obvious privacy concerns). For now we just built the structure and would come back to finishing it off later with the plumbing.&lt;/p&gt;
&lt;p&gt;&lt;img src=&#34;https://wilw.dev/media/blog/bathroom11.jpg&#34; alt=&#34;Building out the structure of the new wall&#34;&gt;
&lt;img src=&#34;https://wilw.dev/media/blog/bathroom12.jpg&#34; alt=&#34;Building out the structure of the new wall&#34;&gt;&lt;/p&gt;
&lt;h2 id=&#34;trying-to-get-the-bath-in&#34;&gt;Trying to get the bath in&lt;/h2&gt;
&lt;p&gt;Probably one of the most challenging parts of this project was getting our ridiculously-shaped new bath into the room.&lt;/p&gt;
&lt;p&gt;In the picture below you can see the two doors into the bathroom. We would later build over the one we (eventually) got the bath through!&lt;/p&gt;
&lt;p&gt;&lt;img src=&#34;https://wilw.dev/media/blog/bathroom13.jpg&#34; alt=&#34;Getting the bathtub through the door&#34;&gt;&lt;/p&gt;
&lt;h2 id=&#34;taking-down-the-old-plasterboard&#34;&gt;Taking down the old plasterboard&lt;/h2&gt;
&lt;p&gt;Removing the walls had caused damage to the old plasterboard. We also wanted to replace some of this with moisture board instead in order to make the walls more water resistant.&lt;/p&gt;
&lt;p&gt;We broke and stripped off the plasterboard from the inner side of the landing wall. In the images below you can also see the structure of the new wall we had started building where the old shower room door was.&lt;/p&gt;
&lt;p&gt;&lt;img src=&#34;https://wilw.dev/media/blog/bathroom14.jpg&#34; alt=&#34;Removing the old plasterboard&#34;&gt;
&lt;img src=&#34;https://wilw.dev/media/blog/bathroom15.jpg&#34; alt=&#34;Removing the old plasterboard&#34;&gt;&lt;/p&gt;
&lt;p&gt;This required a bit of cleaning up. Notice the bath now (somehow) in the room!&lt;/p&gt;
&lt;p&gt;&lt;img src=&#34;https://wilw.dev/media/blog/bathroom10.jpg&#34; alt=&#34;Tidying up after pulling down the plasterboard&#34;&gt;&lt;/p&gt;
&lt;h2 id=&#34;continuing-with-the-wall&#34;&gt;Continuing with the wall&lt;/h2&gt;
&lt;p&gt;With that out of the way, we could then add new plasterboard and moisture board to all of the new and exposed walls.&lt;/p&gt;
&lt;p&gt;&lt;img src=&#34;https://wilw.dev/media/blog/bathroom17.jpg&#34; alt=&#34;Screwing in the moisture boards&#34;&gt;
&lt;img src=&#34;https://wilw.dev/media/blog/bathroom18.jpg&#34; alt=&#34;The completed walls&#34;&gt;&lt;/p&gt;
&lt;h2 id=&#34;some-drainage-plumbing&#34;&gt;Some drainage plumbing&lt;/h2&gt;
&lt;p&gt;We built a small raised platform that would more easily enable us to run drainage under the floor, which you can see in the picture below. We also got the bath plumbed in.&lt;/p&gt;
&lt;p&gt;&lt;img src=&#34;https://wilw.dev/media/blog/bathroom19.jpg&#34; alt=&#34;Building the raised floor structure and plumbing the bath outlet&#34;&gt;&lt;/p&gt;
&lt;h2 id=&#34;finishing-off-the-floor&#34;&gt;Finishing off the floor&lt;/h2&gt;
&lt;p&gt;To complete the floor we added plywood and some simple lino flooring.&lt;/p&gt;
&lt;p&gt;&lt;img src=&#34;https://wilw.dev/media/blog/bathroom20.jpg&#34; alt=&#34;Laying the lino flooring&#34;&gt;
&lt;img src=&#34;https://wilw.dev/media/blog/bathroom21.jpg&#34; alt=&#34;Trimming the lino to size&#34;&gt;&lt;/p&gt;
&lt;p&gt;Proving that the bath works! With inlet and outlet plumbing done.&lt;/p&gt;
&lt;p&gt;&lt;img src=&#34;https://wilw.dev/media/blog/bathroom22.jpg&#34; alt=&#34;The installed bath with running taps&#34;&gt;&lt;/p&gt;
&lt;h2 id=&#34;tiling&#34;&gt;Tiling&lt;/h2&gt;
&lt;p&gt;To complete the wall around the shower, we added tiles in a herringbone style. Naturally we left the hardest bits (i.e. the edges) to last.&lt;/p&gt;
&lt;p&gt;We also added in some of the shower plumbing through the wall so we could tile around it.&lt;/p&gt;
&lt;p&gt;&lt;img src=&#34;https://wilw.dev/media/blog/bathroom23.jpg&#34; alt=&#34;Starting the herrinbone tile pattern with shower head installed&#34;&gt;
&lt;img src=&#34;https://wilw.dev/media/blog/bathroom24.jpg&#34; alt=&#34;Continuing the tiling on all three shower-adjacent walls&#34;&gt;
&lt;img src=&#34;https://wilw.dev/media/blog/bathroom25.jpg&#34; alt=&#34;Finishing off the majority of the tiling&#34;&gt;&lt;/p&gt;
&lt;h2 id=&#34;installing-the-new-shower-tray-and-glass&#34;&gt;Installing the new shower tray and glass&lt;/h2&gt;
&lt;p&gt;Next, we needed to install the shower tray and glass. The shower would be a walk-in shower without a door.&lt;/p&gt;
&lt;p&gt;We also grouted the tiles and sealed the shower tray.&lt;/p&gt;
&lt;p&gt;&lt;img src=&#34;https://wilw.dev/media/blog/bathroom26.jpg&#34; alt=&#34;The new shower tray and glass with grouted tiles&#34;&gt;
&lt;img src=&#34;https://wilw.dev/media/blog/bathroom27.jpg&#34; alt=&#34;The new shower tray and glass with grouted tiles&#34;&gt;&lt;/p&gt;
&lt;h2 id=&#34;building-the-sink&#34;&gt;Building the sink&lt;/h2&gt;
&lt;p&gt;We bought a nice sink bowl and some hairpin legs to build the support for the sink, as you’ll see in the pictures below.&lt;/p&gt;
&lt;p&gt;We also added some skirting rail, dado rail, and some panelling effect for improved aesthetics.&lt;/p&gt;
&lt;p&gt;We plumbed the sink through the wall - just like the shower.&lt;/p&gt;
&lt;p&gt;&lt;img src=&#34;https://wilw.dev/media/blog/bathroom28.jpg&#34; alt=&#34;Building the new sink&#34;&gt;
&lt;img src=&#34;https://wilw.dev/media/blog/bathroom29.jpg&#34; alt=&#34;Building the new sink&#34;&gt;&lt;/p&gt;
&lt;h2 id=&#34;adding-some-paint&#34;&gt;Adding some paint&lt;/h2&gt;
&lt;p&gt;The next jobs were to plaster the remaining exposed plasterboard and add some paint. We also added some additional panelling to the wall.&lt;/p&gt;
&lt;p&gt;As you’ll see from these photos, we went for a dark blue colour for the main part of the wall to match the tiles.&lt;/p&gt;
&lt;p&gt;&lt;img src=&#34;https://wilw.dev/media/blog/bathroom30.jpg&#34; alt=&#34;After plastering and painting the wall&#34;&gt;&lt;/p&gt;
&lt;p&gt;Some more tidying up!&lt;/p&gt;
&lt;p&gt;&lt;img src=&#34;https://wilw.dev/media/blog/bathroom31.jpg&#34; alt=&#34;Mess and clutter on the floor&#34;&gt;&lt;/p&gt;
&lt;h2 id=&#34;closing-off-the-door&#34;&gt;Closing off the door&lt;/h2&gt;
&lt;p&gt;Remember that old shower room? The wall to that still needed to be completed. We patched the hole with some plasterboard, plastered it and its surround, and painted it the same colour as the landing.&lt;/p&gt;
&lt;p&gt;&lt;img src=&#34;https://wilw.dev/media/blog/bathroom32.jpg&#34; alt=&#34;Patching the old shower room doorway&#34;&gt;&lt;/p&gt;
&lt;p&gt;Once done, the old doorway was nowhere to be seen.&lt;/p&gt;
&lt;p&gt;&lt;img src=&#34;https://wilw.dev/media/blog/bathroom33.jpg&#34; alt=&#34;After plastering and painting the old shower room doorway&#34;&gt;&lt;/p&gt;
&lt;h2 id=&#34;finishing-touches&#34;&gt;Finishing touches&lt;/h2&gt;
&lt;p&gt;Finally, we bought some mirrors, pictures, furniture, and some fittings (such as hand-towel rails) for the walls. We also added a rug and some hanging plants, which sort of water themselves from the shower’s condensation.&lt;/p&gt;
&lt;p&gt;We plumbed in a heated towel rail next to the shower, added some coving, and some light strips.&lt;/p&gt;
&lt;p&gt;&lt;img src=&#34;https://wilw.dev/media/blog/bathroom35.jpg&#34; alt=&#34;The finished bathroom, showing new furtniture and fittings&#34;&gt;
&lt;img src=&#34;https://wilw.dev/media/blog/bathroom34.jpg&#34; alt=&#34;The finished bathroom, showing new furtniture and fittings&#34;&gt;
&lt;img src=&#34;https://wilw.dev/media/blog/bathroom36.jpg&#34; alt=&#34;The finished bathroom, showing new furtniture and fittings&#34;&gt;&lt;/p&gt;
</content:encoded>
    </item>
    
    <item>
      <title>Automatically scanning for malicious user-uploaded files</title>
      <link>https://wilw.dev/blog/2021/11/06/auto-av/</link>
      <pubDate>Sat, 06 Nov 2021 15:38:00 +0000</pubDate>
      
      <guid>https://wilw.dev/blog/2021/11/06/auto-av/</guid>
      
        <category>100daystooffload</category>
      
        <category>technology</category>
      
      
      <content:encoded>&lt;p&gt;If you run a service that accepts file uploads from users, and then subsequent re-download by other users (such as images), then your service is potentially at risk of becoming a system for distributing malware. Without safeguards in place, bad actors could potentially use your service to upload harmful files with the intention of them being downloaded by other users.&lt;/p&gt;
&lt;p&gt;Services like Google Drive and some email providers will automatically scan files for malicious payloads, but if you - like many people - rely on more basic object storage for storing files for your apps, then there may be less default protection available.&lt;/p&gt;
&lt;p&gt;Luckily there are a number of methods available for addressing this.&lt;/p&gt;
&lt;h2 id=&#34;overview&#34;&gt;Overview&lt;/h2&gt;
&lt;p&gt;Whilst the concepts are mostly generic and framework/infrastructure agnostic, in this post I’ll focus on a process that leverages Amazon S3, Lambda, and &lt;a href=&#34;https://www.clamav.net&#34; target=&#34;_blank&#34; rel=&#34;noopener noreferrer&#34; &gt;ClamAV&lt;/a&gt;. ClamAV is open-source antimalware software that can be executed as a binary without requiring a GUI.&lt;/p&gt;
&lt;p&gt;In this post I won’t include code (for brevity), but I will walk through the key stages, which are:&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;Periodic refresh of malware/virus definitions&lt;/li&gt;
&lt;li&gt;Running the antimalware check upon new file upload&lt;/li&gt;
&lt;li&gt;Denying uploads to “infected” files.&lt;/li&gt;
&lt;/ol&gt;
&lt;h2 id=&#34;managing-and-refreshing-virus-definitions&#34;&gt;Managing and refreshing virus definitions&lt;/h2&gt;
&lt;p&gt;This stage allows ClamAV to keep up-to-date and recognise the types of files that might be infected. It involves three steps:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;Obtain a binary capable of downloading new definitions&lt;/li&gt;
&lt;li&gt;Write a function to run the binary&lt;/li&gt;
&lt;li&gt;Schedule the function to be run periodically&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;ClamAV provides &lt;a href=&#34;https://docs.clamav.net/manual/Usage/SignatureManagement.html&#34; target=&#34;_blank&#34; rel=&#34;noopener noreferrer&#34; &gt;FreshClam&lt;/a&gt; - a tool for updating and managing a local database of virus signatures.&lt;/p&gt;
&lt;p&gt;In order to obtain a &lt;code&gt;freshclam&lt;/code&gt; binary for use in Lambda, I recommend installing ClamAV on an EC2 instance running Amazon Linux 2, and then extracting the tool from the filesystem (e.g. by running &lt;code&gt;which freshclam&lt;/code&gt;). Note that you may also need some other library files for the binary to work (you’ll notice errors which will be pretty self-explanatory).&lt;/p&gt;
&lt;p&gt;Once you have the &lt;code&gt;freshclam&lt;/code&gt; binary, create and upload a &lt;a href=&#34;https://docs.aws.amazon.com/lambda/latest/dg/configuration-layers.html&#34; target=&#34;_blank&#34; rel=&#34;noopener noreferrer&#34; &gt;Lambda Layer&lt;/a&gt; containing the binary. Depending on the runtime your Lambda function will use, you will need to store the binary at a specific path in your Layer. Give your Layer a suitable name, such as &lt;code&gt;freshclamLayer&lt;/code&gt;.&lt;/p&gt;
&lt;p&gt;Next, we need to create a Lambda function. It doesn’t matter what runtime you use, so long as you can use it to execute the &lt;code&gt;freshclam&lt;/code&gt; binary from the filesystem as some form of subprocess. Once ready, upload it to Lambda and reference the &lt;code&gt;freshclamLayer&lt;/code&gt; Layer so that the binary can be made available to the function.&lt;/p&gt;
&lt;p&gt;The function should outut the generated signature database to an S3 bucket. As such, your function will need to have an IAM role that enables write access to your chosen bucket.&lt;/p&gt;
&lt;p&gt;Finally, use &lt;a href=&#34;https://docs.aws.amazon.com/AmazonCloudWatch/latest/events/WhatIsCloudWatchEvents.html&#34; target=&#34;_blank&#34; rel=&#34;noopener noreferrer&#34; &gt;CloudWatch Events&lt;/a&gt; to schedule your function to be run periodically. For example, you could update the signatures once or twice per day, depending on your needs.&lt;/p&gt;
&lt;h2 id=&#34;running-the-antimalware-check-on-new-files&#34;&gt;Running the antimalware check on new files&lt;/h2&gt;
&lt;p&gt;This stage uses the virus definitions to decide whether a newly-uploaded file might be infected. It involves two key steps:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;Create a Lambda function that runs the virus scanner&lt;/li&gt;
&lt;li&gt;Create an S3 trigger that runs the function when new files are uploaded&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;In addition to FreshClam, ClamAV also provides a tool called &lt;a href=&#34;https://docs.clamav.net/manual/Usage/Scanning.html#one-time-scanning&#34; target=&#34;_blank&#34; rel=&#34;noopener noreferrer&#34; &gt;ClamScan&lt;/a&gt; which can be invoked on specific files or directories in order to check them for malicious content.&lt;/p&gt;
&lt;p&gt;Obtain the &lt;code&gt;clamscan&lt;/code&gt; binary as described in the previous step, and bundle this into a Layer (again, as above), named something like &lt;code&gt;clamscanLayer&lt;/code&gt;.&lt;/p&gt;
&lt;p&gt;Next, create a new Lambda function (again, choose a runtime that can invoke binary subprocesses). The function should check the &lt;code&gt;event&lt;/code&gt; passed to it in order to determine the path to the file that was uploaded, download the previously-uploaded virus signatures from S3, and then run &lt;code&gt;clamscan&lt;/code&gt; against the target file. The output from the binary should be monitored to understand whether the file is suspected to be malicious.&lt;/p&gt;
&lt;p&gt;If &lt;code&gt;clamscan&lt;/code&gt; determines that the file is malicious, then use an S3 tag to mark the file as infected. For example, you could create a tag named &lt;code&gt;Infected&lt;/code&gt; and pass a value of &lt;code&gt;true&lt;/code&gt; or &lt;code&gt;false&lt;/code&gt; depending on the scan’s output. For this, you’ll need to give your function relevant IAM permissions to enable object tagging in S3 and also read access to the bucket where the virus definitions were stored in the previous stage.&lt;/p&gt;
&lt;p&gt;Also make sure that your function uses your &lt;code&gt;clamscanLayer&lt;/code&gt; Layer so that it can access the relevant binary.&lt;/p&gt;
&lt;p&gt;Finally, configure the S3 bucket responsible for holding user uploads in order to add a new trigger that invokes your scanning function each time a new object is put to the bucket.&lt;/p&gt;
&lt;h2 id=&#34;restrict-downloads-of-infected-files&#34;&gt;Restrict downloads of infected files&lt;/h2&gt;
&lt;p&gt;The final stage involves telling S3 to forbid access to infected files. To do so, simply create (or modify) the &lt;a href=&#34;https://docs.aws.amazon.com/AmazonS3/latest/userguide/tagging-and-policies.html&#34; target=&#34;_blank&#34; rel=&#34;noopener noreferrer&#34; &gt;bucket policy&lt;/a&gt; for the user uploads bucket such that it denies the &lt;code&gt;GetObject&lt;/code&gt; action for any objects that have the condition of an &lt;code&gt;Infected&lt;/code&gt; tag with the value of &lt;code&gt;true&lt;/code&gt;.&lt;/p&gt;
&lt;h2 id=&#34;conclusion&#34;&gt;Conclusion&lt;/h2&gt;
&lt;p&gt;In this post I’ve provided a rough overview of a process that allows for scanning user-uploaded files for malicious content. Hopefully this might help if you’re looking to make your services more secure for your users!&lt;/p&gt;
</content:encoded>
    </item>
    
    <item>
      <title>A little trip to A&amp;E</title>
      <link>https://wilw.dev/blog/2021/11/03/shoulder/</link>
      <pubDate>Wed, 03 Nov 2021 18:45:00 +0000</pubDate>
      
      <guid>https://wilw.dev/blog/2021/11/03/shoulder/</guid>
      
        <category>100daystooffload</category>
      
        <category>life</category>
      
      
      <content:encoded>&lt;p&gt;On Sunday I slipped and fell in the pouring rain. I landed hard on my side and ended up dislocating my shoulder.&lt;/p&gt;
&lt;p&gt;I didn’t really want to try risking it and putting it back in myself (sensibly, as it turns out!), so I got a taxi to A&amp;E (an emergency ward in UK NHS hospitals). I was lucky enough to be seen within a few minutes; I then had examinations, had X-rays, and was given gas. When the doctor got round to me, he managed to push it back into position after a few minutes of moving it around - accompanied by a very loud click.&lt;/p&gt;
&lt;p&gt;I was sent home in a sling and now have a couple of weeks of physio ahead. I’m lucky that this was the first time I’ve ever actually needed to visit A&amp;E - for myself, at least.&lt;/p&gt;
&lt;p&gt;It’s a little ironic that I had &lt;a href=&#34;https://wilw.dev/blog/2021/10/30/positive-thinking&#34;&gt;just written about exercising&lt;/a&gt; and keeping active, and now I’m going to be out of full action for a little while!&lt;/p&gt;
&lt;p&gt;Below you can see a picture (taken a few minutes after the fall) showing my left arm a little lower than where it’s meant to be!&lt;/p&gt;
&lt;p&gt;&lt;img src=&#34;https://wilw.dev/media/blog/shoulder.jpg&#34; alt=&#34;Checking out my dodgy shoulder&#34;&gt;&lt;/p&gt;
</content:encoded>
    </item>
    
    <item>
      <title>Thinking positive</title>
      <link>https://wilw.dev/blog/2021/10/30/positive-thinking/</link>
      <pubDate>Sat, 30 Oct 2021 18:45:00 +0000</pubDate>
      
      <guid>https://wilw.dev/blog/2021/10/30/positive-thinking/</guid>
      
        <category>100daystooffload</category>
      
        <category>life</category>
      
      
      <content:encoded>&lt;p&gt;It’s been a weird 18 months. Before pandemic-initiated changes, our daily lives might have involved getting up and travelling on some form of commute (either by walking, public transport, car, or something else) to a place of work each morning, before reversing the process every evening.&lt;/p&gt;
&lt;p&gt;From my perspective, the change to working remotely from home has a number of benefits. Although I do miss seeing people on a daily basis in the office, the &lt;em&gt;flexibility&lt;/em&gt; of remote work certainly outweighs any downsides. This is a feeling also echoed by many across my team - especially those with children or other family life that they need to work around.&lt;/p&gt;
&lt;p&gt;As the months passed, however, it was all too easy to begin to slip into the routine of getting up just before morning meetings began, slide into a t-shirt and joggers, and head over to a desk to begin work for the day.&lt;/p&gt;
&lt;p&gt;Without the work-life separation that the commute and workplace provided, I - like many people - started living, what felt like, a Groundhog Day-style life. I began to care a little less about my appearance, my health, and routine, since every day felt the same.&lt;/p&gt;
&lt;p&gt;I quickly realised that this was not good for my mental wellbeing and general happiness. I usually have quite a positive outlook but I felt myself slipping a little into a mild depression.&lt;/p&gt;
&lt;p&gt;I needed to take action, and I did. These days I feel much more adjusted to mixing home and work life. I’m generally happier and feel positive and pro-active again. Below are some of the things I do to build routine and positivity into my life.&lt;/p&gt;
&lt;h2 id=&#34;get-up-earlier&#34;&gt;Get up earlier&lt;/h2&gt;
&lt;p&gt;Without needing to spend time commuting, it’s easy to get up later in the morning. This can make you feel sluggish and give you a more negative view of your work day ahead.&lt;/p&gt;
&lt;p&gt;I recommend setting your alarm clock earlier. Give your mind time in the morning to wake up. Spend time journaling, reading, talking with family, or anything else. This helps to break the sleep-work-sleep routine.&lt;/p&gt;
&lt;h2 id=&#34;make-your-bed&#34;&gt;Make your bed!&lt;/h2&gt;
&lt;p&gt;This sounds like (and is) a small thing, but taking the time to make your bed each morning, and tidy up a little, helps to restore your own pride in your appearance, as well as to give you a more positive outlook.&lt;/p&gt;
&lt;h2 id=&#34;get-a-morning-routine&#34;&gt;Get a morning routine&lt;/h2&gt;
&lt;p&gt;Treat your day as if you were going to go to a workplace. Have a shower, shave (if that way inclined!), style your hair, choose clothes you like, and so on. You’ll feel better for it than just pulling on some easy pyjamas.&lt;/p&gt;
&lt;p&gt;You’ll also feel more alert during video meetings and will be able to contribute more positively.&lt;/p&gt;
&lt;h2 id=&#34;if-you-need-it-build-some-separation&#34;&gt;If you need it, build some separation&lt;/h2&gt;
&lt;p&gt;Some people find it hard to get their minds into gear to begin work in the morning, especially if they work in the same place in which they live.&lt;/p&gt;
&lt;p&gt;If you are one of these people, then build in some separation. After your shower, go for a quick ten minute walk round the block or a local park, get a coffee, listen to a podcast.&lt;/p&gt;
&lt;h2 id=&#34;exercise&#34;&gt;Exercise&lt;/h2&gt;
&lt;p&gt;Exercise had one of the biggest positve impacts on my daily life. I’ve already &lt;a href=&#34;https://wilw.dev/blog/2021/05/12/running&#34;&gt;posted about running&lt;/a&gt; before, and I think this can be hugely beneficial for one’s mental wellbeing, as well as physical fitness.&lt;/p&gt;
&lt;p&gt;You don’t need to join a gym (though, of course, that can help); just find something you enjoy doing - swimming, walking, or anything else - and that you can do a few times a week in and around your routine.&lt;/p&gt;
&lt;p&gt;Personally I try and run three times a week and do some form of resistance training every day. I find it quite meditative and it’s always a good thing to be a bit fitter.&lt;/p&gt;
&lt;h2 id=&#34;organise-your-day&#34;&gt;Organise your day&lt;/h2&gt;
&lt;p&gt;There’s nothing worse than sitting down to start your work day, and feeling lost with too much (or too little!) to do.&lt;/p&gt;
&lt;p&gt;One way around this is to use to-do lists, and to write down things you hope to achieve during your day. You could write these the day or evening before, or during the extra time you have before work from getting up earlier in the morning.&lt;/p&gt;
&lt;p&gt;There are lots of great tools for this. On my iPhone and Mac I use &lt;a href=&#34;https://www.sortedapp.com&#34; target=&#34;_blank&#34; rel=&#34;noopener noreferrer&#34; &gt;Sorted&lt;/a&gt;, which I find useful for combining tasks and events into one place. Other people use different tools with great success, or even pen and paper. Choose something that works for you.&lt;/p&gt;
&lt;h2 id=&#34;break-up-your-day&#34;&gt;Break up your day&lt;/h2&gt;
&lt;p&gt;If you find it tedious without the natural distractions a normal office and other people povide, then build these in too.&lt;/p&gt;
&lt;p&gt;Go and work from a local coffee shop in the afternoons, have a lunch-time walk, do chores or exercise between meetings - all of these can help introduce variation into your day.&lt;/p&gt;
&lt;h2 id=&#34;in-general&#34;&gt;In general&lt;/h2&gt;
&lt;p&gt;Home (or remote) working is certainly not for everyone. Whilst I prefer it, I can fully appreciate why some may prefer spending more time in the office with other people. If and when (if not already) your team are back in an office then certainly take advantage of it.&lt;/p&gt;
&lt;p&gt;If you will continue to be working from home, and want to mix things up a bit and feel more invigorated - perhaps by following some of the ideas in this post - then I advise finding something that sticks. Routines are a bit like a diet; they only really work if you can continue to live with them and can actually &lt;em&gt;enjoy&lt;/em&gt; them.&lt;/p&gt;
&lt;p&gt;If you’re in this latter boat, I’d love to hear from you, particularly if you have any of your own tips for keeping positive in mixing your home and work life!&lt;/p&gt;
</content:encoded>
    </item>
    
    <item>
      <title>Taking FreeBSD for a spin</title>
      <link>https://wilw.dev/blog/2021/10/29/freebsd/</link>
      <pubDate>Fri, 29 Oct 2021 08:44:00 +0000</pubDate>
      
      <guid>https://wilw.dev/blog/2021/10/29/freebsd/</guid>
      
        <category>100daystooffload</category>
      
        <category>technology</category>
      
      
      <content:encoded>&lt;h2 id=&#34;background&#34;&gt;Background&lt;/h2&gt;
&lt;p&gt;I’ve recently noticed (and read) more and more posts discussing *BSD systems. Creations like the new (and excellent) &lt;a href=&#34;https://webzine.puffy.cafe&#34; target=&#34;_blank&#34; rel=&#34;noopener noreferrer&#34; &gt;OpenBSD Webzine&lt;/a&gt; and blogs (such as &lt;a href=&#34;https://rubenerd.com&#34; target=&#34;_blank&#34; rel=&#34;noopener noreferrer&#34; &gt;Rubenerd’s&lt;/a&gt; and &lt;a href=&#34;https://dataswamp.org/~solene&#34; target=&#34;_blank&#34; rel=&#34;noopener noreferrer&#34; &gt;Solene’s&lt;/a&gt;) do a great job in raising awareness of the family of operating systems.&lt;/p&gt;
&lt;p&gt;I’m pretty familiar and comfortable with Linux, having spent many years using it as a daily driver (I am back on macOS full-time right now). Whilst UNIX systems share a lot of similarities, I’ve never properly used a BSD system before.&lt;/p&gt;
&lt;h2 id=&#34;research&#34;&gt;Research&lt;/h2&gt;
&lt;p&gt;My interest was piqued, and I wanted to try one out for a few days. I did some research for good jumping-off points.&lt;/p&gt;
&lt;p&gt;From what I read, &lt;a href=&#34;https://www.freebsd.org&#34; target=&#34;_blank&#34; rel=&#34;noopener noreferrer&#34; &gt;FreeBSD&lt;/a&gt; seemed the best for someone coming from the outside world for the first time, due to its (generally) wider compatibility with known software. &lt;a href=&#34;https://www.netbsd.org&#34; target=&#34;_blank&#34; rel=&#34;noopener noreferrer&#34; &gt;NetBSD&lt;/a&gt; is designed to work well on a wide range of architectures and platforms, and &lt;a href=&#34;https://www.openbsd.org&#34; target=&#34;_blank&#34; rel=&#34;noopener noreferrer&#34; &gt;OpenBSD&lt;/a&gt; has a strong focus on “correctness” and security. It’s also worth checking out the &lt;a href=&#34;https://en.wikipedia.org/wiki/Comparison_of_BSD_operating_systems&#34; target=&#34;_blank&#34; rel=&#34;noopener noreferrer&#34; &gt;other BSD operating systems&lt;/a&gt; for a wider understanding too.&lt;/p&gt;
&lt;h2 id=&#34;installation&#34;&gt;Installation&lt;/h2&gt;
&lt;p&gt;I thought I’d get going by installing FreeBSD and trying it out for a few days. Unfortunately I did not have any suitable hardware to hand for the installation, as I currently only have a MacBook Pro.&lt;/p&gt;
&lt;p&gt;In the end I opted to just use VirtualBox - it wouldn’t give the “baremetal” experience, but it’d be close enough to get the feel! I downloaded an image from the &lt;a href=&#34;https://www.freebsd.org/where&#34; target=&#34;_blank&#34; rel=&#34;noopener noreferrer&#34; &gt;download page&lt;/a&gt; and it ran without problems.&lt;/p&gt;
&lt;p&gt;During initial experimentation I ran out of storage space on the primary volume quite quickly, and so I recommend extending the virtual disk’s size (which can be done &lt;a href=&#34;https://www.howtogeek.com/124622/how-to-enlarge-a-virtual-machines-disk-in-virtualbox-or-vmware&#34; target=&#34;_blank&#34; rel=&#34;noopener noreferrer&#34; &gt;from within VirtualBox&lt;/a&gt;).&lt;/p&gt;
&lt;h2 id=&#34;impressions&#34;&gt;Impressions&lt;/h2&gt;
&lt;p&gt;Once installed, my initial impressions were great. The system was super easy to set-up and configure. Everything is logically organised and the documentation is fantastic.&lt;/p&gt;
&lt;p&gt;I was able to easily setup a graphical envivonment by installing &lt;a href=&#34;https://docs.freebsd.org/en/books/handbook/x11&#34; target=&#34;_blank&#34; rel=&#34;noopener noreferrer&#34; &gt;X&lt;/a&gt;, display managers and desktop environments. These all worked without a hitch and with simple edits to &lt;code&gt;/etc/rc.conf&lt;/code&gt; to get things starting properly at boot-time.&lt;/p&gt;
&lt;p&gt;I could install Firefox without problems, and sound (e.g. from YouTube videos) seemed to work out of the box. Development tools, such as Node, were also a simple &lt;code&gt;pkg install&lt;/code&gt; away.&lt;/p&gt;
&lt;p&gt;Hats off to the FreeBSD community for creating such a rich set of available packages and for all of the hard work in porting these from other platforms. The overall set-up and usage was simpler, better documented, and more logical than comparative Linux systems, such as Arch (which is still my go-to Linux distro).&lt;/p&gt;
&lt;p&gt;The entire system almost feels like a pleasure to use and administer, and I look forward to learning and trying more.&lt;/p&gt;
&lt;h2 id=&#34;pitfalls&#34;&gt;Pitfalls&lt;/h2&gt;
&lt;p&gt;This may be more related to VirtualBox limitations than FreeBSD itself, but I was unable to route USB devices (such as microphone and webcam) through to applications on FreeBSD. For example, in order to use Google Meet or the Zoom web client. I’d like to try these again on a non-virtualised system when I get the chance.&lt;/p&gt;
&lt;p&gt;Docker is a known missing-entity on FreeBSD, and so I would not be able to build images and run containers directly. However, the concept of &lt;a href=&#34;https://docs.freebsd.org/en/books/handbook/jails&#34; target=&#34;_blank&#34; rel=&#34;noopener noreferrer&#34; &gt;jails&lt;/a&gt; seem super interesting and I would like to investigate these further.&lt;/p&gt;
&lt;p&gt;Aside from these minor things, there is no reason why FreeBSD couldn’t become an excellent daily driver for my work in particular.&lt;/p&gt;
&lt;h2 id=&#34;worth-noting&#34;&gt;Worth noting&lt;/h2&gt;
&lt;p&gt;A few other bits worth mentioning are below.&lt;/p&gt;
&lt;h3 id=&#34;documentation&#34;&gt;Documentation&lt;/h3&gt;
&lt;p&gt;As I’ve already alluded to, the documentation for FreeBSD (and the equivalent for other BSD operating systems too) is fantastic.&lt;/p&gt;
&lt;p&gt;The &lt;a href=&#34;https://docs.freebsd.org/en/books/handbook&#34; target=&#34;_blank&#34; rel=&#34;noopener noreferrer&#34; &gt;handbook&lt;/a&gt; contained all of the answers I needed when spending my week with the system, and it also makes generally interesting reading too!&lt;/p&gt;
&lt;p&gt;There are also very well-attended &lt;a href=&#34;https://forums.freebsd.org&#34; target=&#34;_blank&#34; rel=&#34;noopener noreferrer&#34; &gt;forums&lt;/a&gt;, on which your answer is probably already available without needing to ask the community an additional question.&lt;/p&gt;
&lt;h3 id=&#34;distribution&#34;&gt;Distribution&lt;/h3&gt;
&lt;p&gt;Unlike Linux distributions, which typically ship with a separate kernel and a number of independent packages, FreeBSD (and other BSD systems) is installed as a base system containing kernel, build tools and related software all together. This makes updates and maintenance less fragile and more predictable.&lt;/p&gt;
&lt;h3 id=&#34;ports-system&#34;&gt;Ports system&lt;/h3&gt;
&lt;p&gt;FreeBSD offers two (primary) ways to get new software installed on your system - &lt;a href=&#34;https://docs.freebsd.org/en/books/handbook/ports&#34; target=&#34;_blank&#34; rel=&#34;noopener noreferrer&#34; &gt;ports and packages&lt;/a&gt;.&lt;/p&gt;
&lt;p&gt;The FreeBSD &lt;em&gt;Ports Collection&lt;/em&gt; provides the files and resources required to build the software from source (thus allowing pre-compilation tweaks, if this is of interest) and &lt;em&gt;packages&lt;/em&gt; allow the user to directly install pre-built binaries.&lt;/p&gt;
&lt;p&gt;Since larger pieces of softare can take a long time to build, many new users - myself included - opt for installing packages directly. Advanced users and organisations may even set up their own server and ports collection to automatically pre-compile packages to their own specification.&lt;/p&gt;
&lt;h3 id=&#34;general&#34;&gt;General&lt;/h3&gt;
&lt;p&gt;Seasoned FreeBSD users often describe their systems as “unsurprising” - in a good way. Things are just &lt;em&gt;logical&lt;/em&gt;, they don’t unexpectedly break, the package manager doesn’t introduce conflicts, and everything is stored where you expect it to be.&lt;/p&gt;
&lt;p&gt;One of the principles followed by FreeBSD developers is the &lt;a href=&#34;https://docs.freebsd.org/en/books/handbook/glossary/#pola-glossary&#34; target=&#34;_blank&#34; rel=&#34;noopener noreferrer&#34; &gt;Principle of Least Astonishment (POLA)&lt;/a&gt;, defined as the following:&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;As FreeBSD evolves, changes visible to the user should be kept as unsurprising as possible. For example, arbitrarily rearranging system startup variables in &lt;code&gt;/etc/defaults/rc.conf&lt;/code&gt; violates POLA. Developers consider POLA when contemplating user-visible system changes.&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;FreeBSD Handbook Glossary&lt;/li&gt;
&lt;/ul&gt;
&lt;/blockquote&gt;
&lt;h2 id=&#34;going-forward&#34;&gt;Going forward&lt;/h2&gt;
&lt;p&gt;My next steps are to get hold of some older/cheaper (but well-supported) hardware to properly try out FreeBSD on a more permanent basis. I may also spin up a VPS in the cloud running FreeBSD (&lt;a href=&#34;https://www.digitalocean.com&#34; target=&#34;_blank&#34; rel=&#34;noopener noreferrer&#34; &gt;Digital Ocean&lt;/a&gt; offers these) to try running a couple of my lower-impact services through &lt;em&gt;jails&lt;/em&gt;.&lt;/p&gt;
&lt;p&gt;I also noticed that FreeBSD offers PINE64 images on their &lt;a href=&#34;https://www.freebsd.org/where&#34; target=&#34;_blank&#34; rel=&#34;noopener noreferrer&#34; &gt;downloads page&lt;/a&gt; - I’m intrugued to see what my &lt;a href=&#34;https://wilw.dev/blog/2021/10/02/pinephone-phosh/&#34;&gt;Pinephone&lt;/a&gt; thinks about the system too.&lt;/p&gt;
&lt;p&gt;Beyond this, I’d like to also try OpenBSD, and to more fully understand the differences. The &lt;a href=&#34;https://why-openbsd.rocks/fact&#34; target=&#34;_blank&#34; rel=&#34;noopener noreferrer&#34; &gt;Why OpenBSD Rocks&lt;/a&gt; website has some interesting concepts I’d like to read through.&lt;/p&gt;
</content:encoded>
    </item>
    
    <item>
      <title>A mini loft conversion</title>
      <link>https://wilw.dev/blog/2021/10/24/loft-conversion/</link>
      <pubDate>Sun, 24 Oct 2021 20:43:00 +0000</pubDate>
      
      <guid>https://wilw.dev/blog/2021/10/24/loft-conversion/</guid>
      
        <category>100daystooffload</category>
      
        <category>life</category>
      
      
      <content:encoded>&lt;p&gt;Following on from my &lt;a href=&#34;https://wilw.dev/blog/2021/08/14/garden-renovation&#34;&gt;previous post about renovating our garden&lt;/a&gt;, I wanted to write an entry about another project we’ve recently completed.&lt;/p&gt;
&lt;p&gt;Our home is a Victorian townhouse over three storeys, but the top floor has only one bedroom and a bathroom. The rest of that floor is essentially attic (or loft) space, accessible through a hatch in the ceiling of the middle floor.&lt;/p&gt;
&lt;p&gt;As such, the loft is quite long, and far too big for us to use as-is. In fact, I’m a big believer in that if you &lt;em&gt;need&lt;/em&gt; to put something in the loft then you probably don’t actually need it at all. I’m pretty sure we only have Christmas decorations stored up there.&lt;/p&gt;
&lt;p&gt;This felt like a big waste of space! The loft was a decent height and one could (mostly) stand up in some of it. It also had old fashioned windows in the pitched roof - I have no idea why.&lt;/p&gt;
&lt;p&gt;Based on this, we thought we’d try to convert half of the loft into actually usable house space.&lt;/p&gt;
&lt;p&gt;The rest of this post briefly describes what we did to accomplish this.&lt;/p&gt;
&lt;h2 id=&#34;accessing-the-new-room&#34;&gt;Accessing the new “room”&lt;/h2&gt;
&lt;p&gt;The stairway up to the top floor has a switchback in it. The mini landing at the switch point happens to be at the same level as the loft floor, and we were able to knock through the wall on that landing in order to reveal the loft behind.&lt;/p&gt;
&lt;h2 id=&#34;knocking-through-the-loft-wall&#34;&gt;Knocking through the loft wall&lt;/h2&gt;
&lt;p&gt;As we knocked through, we revealed the loft space. You can see the windows, roof beams and insulation (along with promised bag of Christmas decorations) in the image below. Sorry it’s a bit dark.&lt;/p&gt;
&lt;p&gt;&lt;img src=&#34;https://wilw.dev/media/blog/attic1.jpg&#34; alt=&#34;After knocking through the loft wall to reveal the loft space&#34;&gt;&lt;/p&gt;
&lt;h2 id=&#34;adding-a-door&#34;&gt;Adding a door&lt;/h2&gt;
&lt;p&gt;Since the aim was to create a bedroom, we needed to add a door (for obvious privacy reasons). Luckily we had a spare one lying around from a bathroom project (I may write about this soon!) that we were able to cut to size and shape, and then install within a door frame.&lt;/p&gt;
&lt;p&gt;&lt;img src=&#34;https://wilw.dev/media/blog/attic2.jpg&#34; alt=&#34;Adding a doorframe and door&#34;&gt;&lt;/p&gt;
&lt;p&gt;In the image above you can see the original “sticks” (I’m sure there’s a more technical word for these) that form the wall structure. Afterwards we were able to plaster and paint over this, as shown below.&lt;/p&gt;
&lt;p&gt;&lt;img src=&#34;https://wilw.dev/media/blog/attic3.jpg&#34; alt=&#34;Tidying up the doorway&#34;&gt;&lt;/p&gt;
&lt;h2 id=&#34;back-to-the-room&#34;&gt;Back to the room&lt;/h2&gt;
&lt;p&gt;Next we added a floor to the room, insulated the (new) roof space and added plasterboard to form a celiling and wall.&lt;/p&gt;
&lt;p&gt;In the image below you can see the new floor and the plastered ceiling and far wall. We added hatches to the wall to provide access to the rest of the loft space now behind this room&lt;/p&gt;
&lt;p&gt;&lt;img src=&#34;https://wilw.dev/media/blog/attic5.jpg&#34; alt=&#34;A ceiling, floor, and wall for the loft room&#34;&gt;&lt;/p&gt;
&lt;p&gt;We also hired an electrician to add some nice lighting, switches, and electrical outlets.&lt;/p&gt;
&lt;p&gt;&lt;img src=&#34;https://wilw.dev/media/blog/attic4.jpg&#34; alt=&#34;Lighting and power outlets in the loft room&#34;&gt;&lt;/p&gt;
&lt;h2 id=&#34;finishing-touches&#34;&gt;Finishing touches&lt;/h2&gt;
&lt;p&gt;To finish the room off we washed and coated the brick wall in PVA. This allows us to keep it as an exposed brick wall, but without having dust or dirt coming off from it.&lt;/p&gt;
&lt;p&gt;We also painted the walls, ceiling, and beams, and added some wallpaper. We added some flooring, skirting and some shelves. Finally we added some furniture, and the result is below.&lt;/p&gt;
&lt;p&gt;&lt;img src=&#34;https://wilw.dev/media/blog/attic6.jpg&#34; alt=&#34;The finished loft conversion room&#34;&gt;&lt;/p&gt;
&lt;p&gt;The little window lets through a surprising amount of light. It’s a small bedroom, but it’s cosy and it’s great to have the extra space for when we have visitors!&lt;/p&gt;
</content:encoded>
    </item>
    
    <item>
      <title>Extraterrestrial by Avi Loeb</title>
      <link>https://wilw.dev/blog/2021/10/23/extraterrestrial/</link>
      <pubDate>Sat, 23 Oct 2021 16:42:00 +0000</pubDate>
      
      <guid>https://wilw.dev/blog/2021/10/23/extraterrestrial/</guid>
      
        <category>100daystooffload</category>
      
        <category>book</category>
      
      
      <content:encoded>&lt;p&gt;I often enjoy books that try to take a different view on known events. I don’t mean consipiracy theory - more around thinking laterally or “out of the box”. Such ways of thinking often inspire ideas that drive innovative change, and it’s important in order to counter “group think” or simply accepting what’s easiest.&lt;/p&gt;
&lt;p&gt;One such book that tries to do this is &lt;a href=&#34;https://www.goodreads.com/book/show/48930288-extraterrestrial&#34; target=&#34;_blank&#34; rel=&#34;noopener noreferrer&#34; &gt;Extraterrestrial&lt;/a&gt; by &lt;a href=&#34;https://www.goodreads.com/author/show/19762699.Avi_Loeb&#34; target=&#34;_blank&#34; rel=&#34;noopener noreferrer&#34; &gt;Avi Loeb&lt;/a&gt;.&lt;/p&gt;
&lt;p&gt;&lt;img src=&#34;https://wilw.dev/media/blog/extraterrestrial.jpg&#34; alt=&#34;Extraterrestrial book cover&#34;&gt;&lt;/p&gt;
&lt;p&gt;I finished this book a couple of weeks ago, and have been thinking about it since. Whilst I am not an astronomer - and know very little about the science of astrophysics and the probabilities of celestial events occurring (simply understanding how the distance to the moon is calculated still baffles me) - the themes around the automatic rejection of theory by scientific communities based on pre-conception makes me consider all of the other possible scientific theories we may have thrown away, almost by default, as we strive to learn more as a human community.&lt;/p&gt;
&lt;p&gt;The book’s subtitle is “The First Sign of Intelligent Life Beyond Earth”, and the focus is on the event of an intersetellar object travelling through our solar system back in 2017. Whilst it is not uncommon for objects to be seen moving around our solar system (think planets, asteroids, comets, etc.), the author posits that the particular attributes of &lt;em&gt;this&lt;/em&gt; object are such that it is unlikely that it can be something that was created naturally.&lt;/p&gt;
&lt;p&gt;The author discusses the size and shape of the object, the way it moved on a non-natural trajectory through the system, along with other factors, and essentially asks the question to the scientific community, “why could this not be the creation of an extraterrestrial intelligent community?”.&lt;/p&gt;
&lt;p&gt;The book is about pooling theories and scientific approaches to proving or rejecting them based on available evidence. The author talks about his peers and how they often simply reject extraterrestrial intelligence as the basis for candidate theories simply due to the fact that “because, aliens” is such an unlikely option. Loeb argues that, in these cases, one shouldn’t just reject such theories based on pre-conceived notions of likelihood, but should instead consider all viable theories until the evidence grows sufficiently enough to disprove them.&lt;/p&gt;
&lt;p&gt;Avi Loeb, the author, is a well-published professor of science at Harvard University, and makes robust and compelling arguments whilst trying to remain neutral throughout the book. I enjoyed it; the book is non-technical and is written in a way that makes it easy to consume if you are interested in this space.&lt;/p&gt;
</content:encoded>
    </item>
    
    <item>
      <title>This Week in Tech (TWiT)</title>
      <link>https://wilw.dev/blog/2021/10/17/twit/</link>
      <pubDate>Sun, 17 Oct 2021 20:15:00 +0000</pubDate>
      
      <guid>https://wilw.dev/blog/2021/10/17/twit/</guid>
      
        <category>100daystooffload</category>
      
        <category>technology</category>
      
        <category>podcast</category>
      
        <category>opinion</category>
      
      
      <content:encoded>&lt;p&gt;Another podcast I frequently listen to likely needs no introduction of its own. The &lt;a href=&#34;https://twit.tv&#34; target=&#34;_blank&#34; rel=&#34;noopener noreferrer&#34; &gt;This Week in Tech&lt;/a&gt; (or just “TWiT”) network’s flagship podcast - also called &lt;a href=&#34;https://twit.tv/shows/this-week-in-tech&#34; target=&#34;_blank&#34; rel=&#34;noopener noreferrer&#34; &gt;TWiT&lt;/a&gt; - must be one of the longest-running tech podcasts.&lt;/p&gt;
&lt;p&gt;&lt;img src=&#34;https://wilw.dev/media/blog/twit.jpg&#34; alt=&#34;TWiT cover art&#34;&gt;&lt;/p&gt;
&lt;p&gt;The podcast series started back in 2005. It runs weekly, with episodes recorded live each Sunday evening and made available via podcast clients shortly afterwards.&lt;/p&gt;
&lt;p&gt;It is hosted by &lt;a href=&#34;https://twit.tv/people/leo-laporte&#34; target=&#34;_blank&#34; rel=&#34;noopener noreferrer&#34; &gt;Leo Laporte&lt;/a&gt;, who is joined by interesting and varied panelists from across the tech sector. Episodes feature light-hearted discussion of recent news and insights from the technology world.&lt;/p&gt;
&lt;p&gt;Episodes are pretty lengthy, and they keep me going for a couple of days each week as I walk the dog. However, by listening I feel that I get a really good overview of the relevant news in the space. Leo and the guests are always great to listen to, it’s often funny, and I always learn something new. It’s not too technical and would be easily consumable by anyone interested.&lt;/p&gt;
&lt;p&gt;If you’re looking to add something new to your podcast library, then I can certainly recommend subscribing for general weekly tech updates. I personally listen to it as an audio podcast, but episodes are also available via video.&lt;/p&gt;
</content:encoded>
    </item>
    
    <item>
      <title>Giving a talk at the BSV Wales meet-up</title>
      <link>https://wilw.dev/blog/2021/10/13/wales-bsv/</link>
      <pubDate>Wed, 13 Oct 2021 18:41:00 +0000</pubDate>
      
      <guid>https://wilw.dev/blog/2021/10/13/wales-bsv/</guid>
      
        <category>100daystooffload</category>
      
        <category>technology</category>
      
        <category>life</category>
      
      
      <content:encoded>&lt;p&gt;Last week I gave a talk at the &lt;a href=&#34;https://bitcoinassociation.net&#34; target=&#34;_blank&#34; rel=&#34;noopener noreferrer&#34; &gt;Bitcoin Association&lt;/a&gt; &lt;a href=&#34;https://www.meetup.com/Bitcoin-SV-Wales&#34; target=&#34;_blank&#34; rel=&#34;noopener noreferrer&#34; &gt;BSV Meet-up for Wales&lt;/a&gt;, hosted by &lt;a href=&#34;https://www.tramshedtech.co.uk&#34; target=&#34;_blank&#34; rel=&#34;noopener noreferrer&#34; &gt;Tramshed Tech&lt;/a&gt; in Cardiff.&lt;/p&gt;
&lt;p&gt;&lt;img src=&#34;https://wilw.dev/media/blog/bsv-wales.jpg&#34; alt=&#34;BSV Wales Image&#34;&gt;&lt;/p&gt;
&lt;p&gt;Before learning about this meetup, I had not heard of &lt;a href=&#34;https://bitcoinsv.com&#34; target=&#34;_blank&#34; rel=&#34;noopener noreferrer&#34; &gt;BSV&lt;/a&gt; - either from a technology or currency perspective. However, as well as promoting an interesting project, the event welcomes showcases from technologists working across the blockchain space.&lt;/p&gt;
&lt;p&gt;At &lt;a href=&#34;https://www.simplydo.co.uk&#34; target=&#34;_blank&#34; rel=&#34;noopener noreferrer&#34; &gt;Simply Do&lt;/a&gt; we have recently completed a project that aimed to leverage blockchain distributed ledger technology to help protect and manage IP assets in complex international and cross-domain supply chains. The project was a success and this was what I - along with my colleague, John - presented about.&lt;/p&gt;
&lt;p&gt;&lt;img src=&#34;https://wilw.dev/media/blog/bsv-wales-2.jpg&#34; alt=&#34;Myself talking (animatedly) in the panel&#34;&gt;&lt;/p&gt;
&lt;p&gt;The other talk at the event was around the circular economy approaches undertaken at &lt;a href=&#34;http://www.celsauk.com&#34; target=&#34;_blank&#34; rel=&#34;noopener noreferrer&#34; &gt;Celsa Steel&lt;/a&gt;, which was a mostly-new concept to me, and certainly highly interesting and topical at the moment.&lt;/p&gt;
&lt;p&gt;It was great to be back at in-person events after 18 months of remote-only meetups.  There were some excellent questions and comments, I met some interesting people from around the blockchain space, and I look forward to attending more in-person as events continue to open-up.&lt;/p&gt;
</content:encoded>
    </item>
    
    <item>
      <title>Dotty</title>
      <link>https://wilw.dev/blog/2021/10/11/dotty/</link>
      <pubDate>Mon, 11 Oct 2021 17:19:00 +0000</pubDate>
      
      <guid>https://wilw.dev/blog/2021/10/11/dotty/</guid>
      
        <category>100daystooffload</category>
      
        <category>technology</category>
      
        <category>project</category>
      
      
      <content:encoded>&lt;p&gt;A few years ago I was in the position of needing a solution to backup and sync dotfiles (configuration files for various pieces of software) across my machines.&lt;/p&gt;
&lt;p&gt;Specifically, I had Mac computers and Linux servers, and needed a way to nicely keep these files up-to-date between them. For example, I may have spent some time crafting and tweaking files - such as my &lt;code&gt;.vimrc&lt;/code&gt; and &lt;code&gt;.tmux.conf&lt;/code&gt; - and needed a way of ensuring all of my devices could access the latest version of these files.&lt;/p&gt;
&lt;p&gt;A few other tools exist for this - such as &lt;a href=&#34;https://github.com/lra/mackup&#34; target=&#34;_blank&#34; rel=&#34;noopener noreferrer&#34; &gt;Mackup&lt;/a&gt; and &lt;a href=&#34;https://github.com/technicalpickles/homesick&#34; target=&#34;_blank&#34; rel=&#34;noopener noreferrer&#34; &gt;homesick&lt;/a&gt; - but I wanted something quick and easy to get off the ground and distribute to other machines. Preferably a single binary.&lt;/p&gt;
&lt;p&gt;This gave me the idea for &lt;a href=&#34;https://dotty.cloud&#34; target=&#34;_blank&#34; rel=&#34;noopener noreferrer&#34; &gt;Dotty&lt;/a&gt; - a hosted manager for dotfile-syncing across devices. It allows for downloading a single binary (pre-compiled for Linux and MacOS devices), from which one can login and pull/push configuration files as needed.&lt;/p&gt;
&lt;p&gt;&lt;img src=&#34;https://wilw.dev/media/blog/dotty.png&#34; alt=&#34;Dotty&amp;rsquo;s website header&#34;&gt;&lt;/p&gt;
&lt;p&gt;The service also exposes a RESTful HTTP API for integrating into other workflows. Both the client and the backend are written in &lt;a href=&#34;https://golang.org&#34; target=&#34;_blank&#34; rel=&#34;noopener noreferrer&#34; &gt;Go&lt;/a&gt;, and backed by a &lt;a href=&#34;https://www.mongodb.com&#34; target=&#34;_blank&#34; rel=&#34;noopener noreferrer&#34; &gt;MongoDB&lt;/a&gt; database.&lt;/p&gt;
&lt;p&gt;Whilst I try to maintain Dotty when I can, and keep the service running for those who still use it, I personally don’t use it anymore and so do not plan to add additional features. These days, I think tools like &lt;a href=&#34;https://syncthing.net&#34; target=&#34;_blank&#34; rel=&#34;noopener noreferrer&#34; &gt;Syncthing&lt;/a&gt; and &lt;a href=&#34;https://nextcloud.com&#34; target=&#34;_blank&#34; rel=&#34;noopener noreferrer&#34; &gt;Nextcloud&lt;/a&gt; offer more robustness, security, and flexibility, and I prefer using these for ease of use.&lt;/p&gt;
</content:encoded>
    </item>
    
    <item>
      <title>This is Going to Hurt by Adam Kay</title>
      <link>https://wilw.dev/blog/2021/10/06/this-is-going-to-hurt/</link>
      <pubDate>Wed, 06 Oct 2021 20:27:00 +0000</pubDate>
      
      <guid>https://wilw.dev/blog/2021/10/06/this-is-going-to-hurt/</guid>
      
        <category>100daystooffload</category>
      
        <category>book</category>
      
      
      <content:encoded>&lt;p&gt;Having recently read &lt;a href=&#34;https://wilw.dev/blog/2021/09/30/secret-barrister&#34;&gt;The Secret Barrister&lt;/a&gt;, which I loved, I was recommended to also check out &lt;a href=&#34;https://www.goodreads.com/book/show/35510008-this-is-going-to-hurt&#34; target=&#34;_blank&#34; rel=&#34;noopener noreferrer&#34; &gt;This is Going to Hurt&lt;/a&gt; by &lt;a href=&#34;https://www.goodreads.com/author/show/17160706.Adam_Kay&#34; target=&#34;_blank&#34; rel=&#34;noopener noreferrer&#34; &gt;Adam Kay&lt;/a&gt;.&lt;/p&gt;
&lt;p&gt;&lt;img src=&#34;https://wilw.dev/media/blog/this_is_going_to_hurt.jpg&#34; alt=&#34;This is Going to Hurt cover&#34;&gt;&lt;/p&gt;
&lt;p&gt;The book is similar to the Secret Barrister in that it’s a collection of insights and stories from a working professional - this time a hospital doctor. The book is subtitled &lt;em&gt;Secret Diaries of a Junior Doctor&lt;/em&gt;.&lt;/p&gt;
&lt;p&gt;The author tells the story of his experiences in completing medical school and beginning work in the UK National Health Service (NHS) system. NHS doctors generally follow a pre-defined pathway from “F1” through to consultant (or slightly different if a GP), and this book describes experiences of the author as he works his way through this process.&lt;/p&gt;
&lt;p&gt;Whilst he spends most of his time in obstetrics and gynaecology healthcare, it is clear that his experiences reflect the entire profession as a whole. The book is well-written and funny, yet the author effectively conveys the every-day stresses, the sleepless weeks and non-sensical shift patterns, and the (literal) life-and-death responsibilities thrown at junior doctors every day.&lt;/p&gt;
&lt;p&gt;The fact that junior doctors can’t just “clock off” when their shift ends (what if they’re half way through an emergency 4 hour operation?), that any over-time is not paid, that the majority of social events are just not feasible for them to join - these all mount up in unimaginable ways for young medics in their mid-twenties.&lt;/p&gt;
&lt;p&gt;Of course there are very rewarding moments too, and the author makes it very clear that this is what most doctors stay in their jobs for: the responsibility to actually help people and make positive differences in other people’s lives. It’s definitely not for the money.&lt;/p&gt;
&lt;p&gt;I come from a family of doctors myself, but this book really throws things into perspective as an honest reflection of the publicly-unseen time, effort, and energy that goes into the NHS from its own people.&lt;/p&gt;
&lt;p&gt;This is another book I can certainly recommend.&lt;/p&gt;
</content:encoded>
    </item>
    
    <item>
      <title>Pinephone: Manjaro and Phosh</title>
      <link>https://wilw.dev/blog/2021/10/02/pinephone-phosh/</link>
      <pubDate>Sat, 02 Oct 2021 15:04:00 +0000</pubDate>
      
      <guid>https://wilw.dev/blog/2021/10/02/pinephone-phosh/</guid>
      
        <category>100daystooffload</category>
      
        <category>technology</category>
      
        <category>pinephone</category>
      
      
      <content:encoded>&lt;p&gt;It’s been a few weeks since my &lt;a href=&#34;https://wilw.dev/blog/2021/08/09/pinephone-first-few-weeks&#34;&gt;last post&lt;/a&gt; about the Pinephone. Since then I have been playing further with a different graphical shell and have been trying out new applications.&lt;/p&gt;
&lt;p&gt;In that previous post, I noted a few points that made the phone tricky to use as a daily-driver. However, it should be noted that this was (intentionally) based purely on the phone’s out-of-the-box configuration. I fully meant to continue exploring and to discover ways in which the device could become more of a useful daily use phone for me. This post forms part of that journey.&lt;/p&gt;
&lt;h2 id=&#34;installing-manjaro-with-phosh&#34;&gt;Installing Manjaro with Phosh&lt;/h2&gt;
&lt;p&gt;Over the past few weeks I have been playing more with the &lt;a href=&#34;https://puri.sm/posts/phosh-overview&#34; target=&#34;_blank&#34; rel=&#34;noopener noreferrer&#34; &gt;Phosh&lt;/a&gt; graphical shell, still running on Manjaro. Phosh is developed by &lt;a href=&#34;https://puri.sm&#34; target=&#34;_blank&#34; rel=&#34;noopener noreferrer&#34; &gt;Purism&lt;/a&gt;, primarily with the aim of creating a useful mobile shell for their &lt;a href=&#34;https://puri.sm/products/librem-5&#34; target=&#34;_blank&#34; rel=&#34;noopener noreferrer&#34; &gt;Librem 5&lt;/a&gt; phone.&lt;/p&gt;
&lt;p&gt;To install this system on the Pinephone, I &lt;a href=&#34;https://github.com/manjaro-pinephone/phosh/releases&#34; target=&#34;_blank&#34; rel=&#34;noopener noreferrer&#34; &gt;downloaded the appropriate image&lt;/a&gt;, &lt;code&gt;dd&lt;/code&gt;’d the image to an SD card (as described &lt;a href=&#34;https://wiki.pine64.org/index.php/PinePhone_Installation_Instructions#Installation_to_the_microSD&#34; target=&#34;_blank&#34; rel=&#34;noopener noreferrer&#34; &gt;in the Pine64 Wiki&lt;/a&gt;), and booted the phone from the SD card.&lt;/p&gt;
&lt;p&gt;The &lt;a href=&#34;https://wiki.pine64.org/index.php/PinePhone_Installation_Instructions#Boot_priority&#34; target=&#34;_blank&#34; rel=&#34;noopener noreferrer&#34; &gt;boot priority&lt;/a&gt; of the device dictates that the phone will initially try and boot from the SD card by default, which is handy if you have a few SD cards lying around with different configurations (like me!).&lt;/p&gt;
&lt;p&gt;Once booted, the default user (&lt;code&gt;manjaro&lt;/code&gt;) has the password &lt;code&gt;123456&lt;/code&gt; - this allows for unlocking the device from the lock screen.&lt;/p&gt;
&lt;h2 id=&#34;initial-impressions&#34;&gt;Initial impressions&lt;/h2&gt;
&lt;p&gt;The first thing that struck me is the speed and responsiveness of the new shell. It is significantly faster than the default Plasma Mobile interface - both in terms of application launch time and in general touch responsiveness throughout the UI.&lt;/p&gt;
&lt;p&gt;This enhancement alone makes the device much more of a pleasure to use; following my intentions in real-time.&lt;/p&gt;
&lt;h2 id=&#34;application-usability&#34;&gt;Application usability&lt;/h2&gt;
&lt;p&gt;Applications that were previously unusable, due to scaling, now seem to work much better. For example, Bitwarden (installed through the “Software” package) now scales much more nicely to the phone’s screen, allowing me to properly login (as shown in the image below).&lt;/p&gt;
&lt;p&gt;&lt;img src=&#34;https://wilw.dev/media/blog/pinephone-phosh-bitwarden.jpg&#34; alt=&#34;Bitwarden login screen shown on Pinephone&#34;&gt;&lt;/p&gt;
&lt;p&gt;Another key improvement is the ability to copy and paste text between applications. I just could not get this to work under Plasma Mobile (maybe this was just me?), but this change now lets me copy and paste passwords from Bitwarden, and also allows me to copy login tokens from a browser.&lt;/p&gt;
&lt;p&gt;&lt;img src=&#34;https://wilw.dev/media/blog/pinephone-phosh-copy.jpg&#34; alt=&#34;Firefox web browser showing a context menu allowing for copying text&#34;&gt;&lt;/p&gt;
&lt;p&gt;This has enabled me to fully login to &lt;a href=&#34;https://github.com/bleakgrey/tootle&#34; target=&#34;_blank&#34; rel=&#34;noopener noreferrer&#34; &gt;Tootle&lt;/a&gt; - a great application for Mastodon. I can view my home, local, and federated timelines, and I can easily post toots (including images with text descriptions).&lt;/p&gt;
&lt;p&gt;&lt;img src=&#34;https://wilw.dev/media/blog/pinephone-phosh-tootle.jpg&#34; alt=&#34;Tootle app showing my local timeline&#34;&gt;&lt;/p&gt;
&lt;p&gt;I can also now change the playback speed within the &lt;a href=&#34;https://wiki.gnome.org/Apps/Podcasts&#34; target=&#34;_blank&#34; rel=&#34;noopener noreferrer&#34; &gt;Podcasts&lt;/a&gt; app - another great quality of life improvement. Before, the UI would not let me select the appropriate radio button.&lt;/p&gt;
&lt;p&gt;&lt;img src=&#34;https://wilw.dev/media/blog/pinephone-phosh-podcasts.jpg&#34; alt=&#34;Podcasts app showing the ability to change playback speed&#34;&gt;&lt;/p&gt;
&lt;h2 id=&#34;other-improvements&#34;&gt;Other improvements&lt;/h2&gt;
&lt;p&gt;Beyond these, I can now use the &lt;a href=&#34;https://wiki.gnome.org/Apps/Geary&#34; target=&#34;_blank&#34; rel=&#34;noopener noreferrer&#34; &gt;Geary&lt;/a&gt; application to log into my Google Mail accounts, which works well.&lt;/p&gt;
&lt;p&gt;I also installed the “Discover” application (from “Software Manager”), which allows easy discovery of apps from &lt;a href=&#34;https://flathub.org/home&#34; target=&#34;_blank&#34; rel=&#34;noopener noreferrer&#34; &gt;Flathub&lt;/a&gt;. Alternatively the &lt;a href=&#34;https://flatpak.org&#34; target=&#34;_blank&#34; rel=&#34;noopener noreferrer&#34; &gt;Flatpak&lt;/a&gt; command can be used to install these from the terminal (or via SSH).&lt;/p&gt;
&lt;p&gt;&lt;img src=&#34;https://wilw.dev/media/blog/pinephone-phosh-lockscreen.jpg&#34; alt=&#34;Pinephone lock-screen, showing Podcasts control&#34;&gt;&lt;/p&gt;
&lt;p&gt;Other quality of life features allow for quick media control actions from the lock-screen (a feature which also worked under Plasma Mobile), and an easy-to-use app-switcher.&lt;/p&gt;
&lt;p&gt;&lt;img src=&#34;https://wilw.dev/media/blog/pinephone-phosh-switcher.jpg&#34; alt=&#34;Phosh app-switcher interface&#34;&gt;&lt;/p&gt;
&lt;h2 id=&#34;moving-forward&#34;&gt;Moving forward&lt;/h2&gt;
&lt;p&gt;I’ve really enjoyed using the phone under Phosh, and I am looking forward to continue to experiment with new configurations, software, and processes. For example, setting up &lt;a href=&#34;https://syncthing.net&#34; target=&#34;_blank&#34; rel=&#34;noopener noreferrer&#34; &gt;Syncthing&lt;/a&gt; to automatically make backups of photos and files, taking advantage of the Nextcloud integration, and - just generally - building out processes to improve daily life with the phone.&lt;/p&gt;
&lt;p&gt;As always, any recommendations are very welcome! Please just get in touch.&lt;/p&gt;
</content:encoded>
    </item>
    
    <item>
      <title>The Secret Barrister: Stories of the Law and How It&#39;s Broken</title>
      <link>https://wilw.dev/blog/2021/09/30/secret-barrister/</link>
      <pubDate>Thu, 30 Sep 2021 16:58:00 +0000</pubDate>
      
      <guid>https://wilw.dev/blog/2021/09/30/secret-barrister/</guid>
      
        <category>100daystooffload</category>
      
        <category>book</category>
      
      
      <content:encoded>&lt;p&gt;&lt;a href=&#34;https://www.goodreads.com/book/show/36620738-the-secret-barrister&#34; target=&#34;_blank&#34; rel=&#34;noopener noreferrer&#34; &gt;The Secret Barrister: Stories of the Law and How It’s Broken&lt;/a&gt; is an oustanding book. In my opinion it is easily the best book I have read in the past year - certainly the most interesting.&lt;/p&gt;
&lt;p&gt;&lt;img src=&#34;https://wilw.dev/media/blog/secret_barrister.jpg&#34; alt=&#34;The Secret Barrister book cover&#34;&gt;&lt;/p&gt;
&lt;p&gt;The book is written by an anonymous barrister working in the criminal justice system for England and Wales. They explain the current inadequacies of criminal justice through a mix of interesting real-life and often first-hand stories.&lt;/p&gt;
&lt;p&gt;I found the history of legal practice super interesting. The author also explains various terms and processes - many of which I had heard of before but never really fully understood the meaning.&lt;/p&gt;
&lt;p&gt;Mostly, I found the book shocking. Many people are unlikely aware (myself included, previously) of the current state of the legal system. The levels to which it is under-funded, under-resourced, and - as a result - the volume of mismanaged cases that can easily result in innocent people getting remanded and guilty people walking free.&lt;/p&gt;
&lt;p&gt;The author’s self-coined “&lt;em&gt;stack-’em-high, sell-’em-cheap&lt;/em&gt;” model sums it up perfectly, in which magistrates and the crown court system - including the CPS - are just not given the time and resources needed to deliver accurate and honest justice.&lt;/p&gt;
&lt;p&gt;The book is extremely well-written (and narrated - my version by Jack Hawkins), clever, and witty. It’s a book I can recommend to anyone. I just hope I never need to be involved in the UK criminal justice system!&lt;/p&gt;
</content:encoded>
    </item>
    
    <item>
      <title>Two months with Duolingo</title>
      <link>https://wilw.dev/blog/2021/09/25/duolingo/</link>
      <pubDate>Sat, 25 Sep 2021 19:10:00 +0000</pubDate>
      
      <guid>https://wilw.dev/blog/2021/09/25/duolingo/</guid>
      
        <category>100daystooffload</category>
      
        <category>life</category>
      
      
      <content:encoded>&lt;h2 id=&#34;english-speakers-privilege&#34;&gt;English speakers’ privilege&lt;/h2&gt;
&lt;p&gt;I’ve always been crap at learning languages. From an early age my parents would encourage me to learn French, and I picked up Spanish and German at around the GCSE level too (exams we take around the age of 16 in the UK). But things just didn’t really ever sink in.&lt;/p&gt;
&lt;p&gt;Part of this would have definitely been down to a childhood unappreciated privilege of understanding English as a first language. As I moved more into the technology domain to follow my interests (and then later for education and work), I was lucky that everything I needed was also in English (from programming languages, technologies, documentation, and more) - of course driven primarily by the US big-tech sector.&lt;/p&gt;
&lt;p&gt;When I used to play online games as a teenager I’d chat to people who’d never even been to an English-speaking country, yet their grasp of the language far exceeded even my own as a native speaker. When I went to university, and more specifically in my post-grad studies, I met people from around the world and everyone would easily be able to speak to each other in English. Even if everyone in my student group spoke another language more fluently, they’d use English for my benefit.&lt;/p&gt;
&lt;p&gt;Whilst these were, of course, acts of kindness, I would always feel embarrassed, uncultured, and often just plain ignorant. Here were people who had made an active effort to learn (at least) one additional language on top of everything else they had going on in their lives as children, teenagers, and young adults. Personally, I could just about tell someone the location of the local library in Spanish (and perhaps order a few drinks), but that was essentially my limit.&lt;/p&gt;
&lt;p&gt;Over the past few years I’ve been spending a bit more time in Spain - encouraged mainly by an increased family presence there. This summer I made the decision to actively try and increase my Spanish language ability so I could do more than simply “get by”.&lt;/p&gt;
&lt;h2 id=&#34;duolingo&#34;&gt;Duolingo&lt;/h2&gt;
&lt;p&gt;Shortly before &lt;a href=&#34;https://www.duolingo.com&#34; target=&#34;_blank&#34; rel=&#34;noopener noreferrer&#34; &gt;Duolingo&lt;/a&gt; &lt;a href=&#34;https://www.sec.gov/Archives/edgar/data/1562088/000162828021013065/duolingos-1.htm&#34; target=&#34;_blank&#34; rel=&#34;noopener noreferrer&#34; &gt;went public&lt;/a&gt; earlier this year, I re-downloaded the app (having used it briefly for Welsh a few years back) and committed myself to it by purchasing the &lt;a href=&#34;https://www.duolingo.com/plus&#34; target=&#34;_blank&#34; rel=&#34;noopener noreferrer&#34; &gt;Plus membership&lt;/a&gt;. I know there are other players in the space (such as &lt;a href=&#34;https://babbel.com&#34; target=&#34;_blank&#34; rel=&#34;noopener noreferrer&#34; &gt;Babbel&lt;/a&gt;), but some of my family were already using Duolingo and I thought the social aspect might keep me more motivated.&lt;/p&gt;
&lt;p&gt;Since July I’ve used the app every day to learn Spanish, and whilst some of it seems familiar from my school days, I’ve learned a huge amount in the relatively short time I’ve been using it.&lt;/p&gt;
&lt;p&gt;Things just seem to sink in much more. Sure, it gets a little repetitive, but the logic and the syntax of the language has become much more clear to me in a way that it never did before. The tips that go alongside each set of lessons explain things succinctly and with good examples.&lt;/p&gt;
&lt;p&gt;&lt;img src=&#34;https://wilw.dev/media/blog/duolingo.png&#34; alt=&#34;My phone&amp;rsquo;s battery usage interface, showing Duolingo at the top&#34;&gt;&lt;/p&gt;
&lt;p&gt;The app has quickly become pretty addictive (though in a way that I don’t mind), and is probably my most-used application (certainly by “battery time”, as shown in the image above!). The “lightning” rounds (where you have to complete a lesson in a fixed amount of time) are super-fun and the “legendary” lessons (where you get three “lives” with which to prove you understand the latest learnings) add lots of extra tension.&lt;/p&gt;
&lt;p&gt;It’s a way of learning that I didn’t expect to be so engaging and effective - for me, at least. It seems as though this model of learning is flexible enough to be used to teach other fact-based subjects, such as history or geography (or even science). It’s also elastic enough to support &lt;em&gt;personalised&lt;/em&gt; learning, adapting well to the user’s own pace and ability.&lt;/p&gt;
&lt;p&gt;Whilst it tries to encourage listening and speaking the language too, and also includes “audio lessons”, it’s very much focused on literacy and understanding. I think I would still struggle to hold anything more than a very basic conversation, but having the foundation in place (and a bit of confidence to try it out more when out and about!) will I hope help with this.&lt;/p&gt;
&lt;p&gt;I look forward to continuing my learning!&lt;/p&gt;
</content:encoded>
    </item>
    
    <item>
      <title>Accessibility is for everyone</title>
      <link>https://wilw.dev/blog/2021/09/22/accessibility-for-everyone/</link>
      <pubDate>Wed, 22 Sep 2021 19:30:00 +0000</pubDate>
      
      <guid>https://wilw.dev/blog/2021/09/22/accessibility-for-everyone/</guid>
      
        <category>100daystooffload</category>
      
        <category>technology</category>
      
        <category>opinion</category>
      
      
      <content:encoded>&lt;p&gt;For many developers, the notion of &lt;em&gt;adding&lt;/em&gt; accessibility features - such as image &lt;code&gt;alt&lt;/code&gt; text attributes to web page images and integrations with host usability enhancements, such as screen-zoom and text-to-speech - might feel like a chore. Especially for those still in the startup or “do things that don’t scale” phase.&lt;/p&gt;
&lt;p&gt;It should no longer be about “adding” accessibility features any more than one these days “adds” a mobile-friendly version of their site (long-surpassed by responsive design and mobile-first pricinples) or even “adds” a button to perform a specific task. Accessibility features are a core part of any product, and should factor into the software development process right through from requirement engineering through to planning, design, implementation, and testing.&lt;/p&gt;
&lt;p&gt;Avoiding accessibility requirements is not only exclusionary and selfish; it results in sub-standard software. If you’re developing an MVP solution or software purely to be tested, by ignoring accessibility needs you automatically exclude whole segments of your audience, whose valuable insights and inputs at the important early or late QA phases will never make it into production before it’s too late.&lt;/p&gt;
&lt;p&gt;It’s &lt;a href=&#34;https://www.computerweekly.com/feature/How-diversity-spurs-creativity-in-software-development&#34; target=&#34;_blank&#34; rel=&#34;noopener noreferrer&#34; &gt;been well-known&lt;/a&gt; for a while that diversity in the software development process enhances the end product and creates a better and more well-rounded solution for everyone.&lt;/p&gt;
&lt;p&gt;On this point, the other thing about accessibility features is that they improve usability &lt;em&gt;for all&lt;/em&gt;, and gives everyone more freedom. Some people rely on the features and supporting technology more than others, but we can all take advantage of it. There is no downside to its inclusion in software development. As well as improving the experience for disabled people, accessibility factors can enhance usability for those using smaller screens, text-based browsers, or people on slow connections.&lt;/p&gt;
&lt;p&gt;Going back to the example near the start of this post, one advantage of &lt;code&gt;alt&lt;/code&gt; text attributes, and semantic markup, are that they empower screen readers to perform their job more accurately and helpfully to their user. Technology exists to help us, and companies like Apple and Amazon take advantage of this now to &lt;a href=&#34;https://support.apple.com/en-gb/HT210406&#34; target=&#34;_blank&#34; rel=&#34;noopener noreferrer&#34; &gt;announce messages to us via AirPods whilst out on a run&lt;/a&gt; and to &lt;a href=&#34;https://www.amazon.com/gp/help/customer/display.html?nodeId=G66R7GDSL86KNFUN&#34; target=&#34;_blank&#34; rel=&#34;noopener noreferrer&#34; &gt;tell us about our day ahead&lt;/a&gt; perhaps whilst driving to work - amongst a growing array of usability and quality-of-life features.&lt;/p&gt;
&lt;p&gt;It’s not just big tech that are expected to fulfil these needs (though we can learn from their examples). Many modern and popular frameworks today - such as the &lt;a href=&#34;https://github.com/facebook/create-react-app&#34; target=&#34;_blank&#34; rel=&#34;noopener noreferrer&#34; &gt;Create React App&lt;/a&gt; project - include code linting rules that help to enforce various accessibility traits for content like images and emoji. Additionally, UI component libraries, such as &lt;a href=&#34;https://getbootstrap.com/docs/4.0/getting-started/accessibility&#34; target=&#34;_blank&#34; rel=&#34;noopener noreferrer&#34; &gt;Bootstrap&lt;/a&gt;, automatically include various &lt;a href=&#34;https://developer.mozilla.org/en-US/docs/Web/Accessibility/ARIA&#34; target=&#34;_blank&#34; rel=&#34;noopener noreferrer&#34; &gt;&lt;code&gt;aria&lt;/code&gt; attributes&lt;/a&gt; in their components. This makes it easy for early-stage startups - or one-person teams - to enhance usability.&lt;/p&gt;
&lt;p&gt;You yourself may not &lt;em&gt;need&lt;/em&gt; to use accessibility features right now, but it’s likely you will at some stage in your life - especially as interfaces continue to develop in complexity through to enhanced gestures, spatial environments, and AR and VR. It’s up to all of us to encourage and practise accessibility-focused development so that it remains a growing priority in the decades to come.&lt;/p&gt;
&lt;p&gt;My website is by no means perfect and should not (yet!) be an example to follow. However I continue to learn more about the space and endeavour to maximise usability and accessibility through factors like &lt;code&gt;alt&lt;/code&gt; text, simple interfaces, using plain and semantic markup, improved reader mode optimisation, and by making all blog content available via RSS.&lt;/p&gt;
</content:encoded>
    </item>
    
    <item>
      <title>Pacman: signature is unknown trust</title>
      <link>https://wilw.dev/blog/2021/09/19/arch-pacman-signatures/</link>
      <pubDate>Sun, 19 Sep 2021 12:15:00 +0000</pubDate>
      
      <guid>https://wilw.dev/blog/2021/09/19/arch-pacman-signatures/</guid>
      
        <category>100daystooffload</category>
      
        <category>technology</category>
      
      
      <content:encoded>&lt;p&gt;I was performing a standard system upgrade on an Arch server this morning and received the following messages (maintainer details redacted):&lt;/p&gt;
&lt;div class=&#34;highlight&#34;&gt;&lt;pre tabindex=&#34;0&#34; style=&#34;background-color:#f0f3f3;-moz-tab-size:2;-o-tab-size:2;tab-size:2;&#34;&gt;&lt;code class=&#34;language-shell&#34; data-lang=&#34;shell&#34;&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;$ sudo pacman -Syyu
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;... &lt;span style=&#34;color:#09f;font-style:italic&#34;&gt;# Download of packages&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;&lt;span style=&#34;color:#555&#34;&gt;(&lt;/span&gt;159/159&lt;span style=&#34;color:#555&#34;&gt;)&lt;/span&gt; checking keys in keyring                 &lt;span style=&#34;color:#555&#34;&gt;[&lt;/span&gt;&lt;span style=&#34;color:#09f;font-style:italic&#34;&gt;######################] 100%&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;&lt;span style=&#34;color:#555&#34;&gt;(&lt;/span&gt;159/159&lt;span style=&#34;color:#555&#34;&gt;)&lt;/span&gt; checking package integrity               &lt;span style=&#34;color:#555&#34;&gt;[&lt;/span&gt;&lt;span style=&#34;color:#09f;font-style:italic&#34;&gt;######################] 100%&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;error: fail2ban: signature from &lt;span style=&#34;color:#c30&#34;&gt;&#34;... &lt;...&gt;&#34;&lt;/span&gt; is unknown trust
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;:: File /var/cache/pacman/pkg/fail2ban-0.11.2-2-any.pkg.tar.zst is corrupted &lt;span style=&#34;color:#555&#34;&gt;(&lt;/span&gt;invalid or corrupted package &lt;span style=&#34;color:#555&#34;&gt;(&lt;/span&gt;PGP signature&lt;span style=&#34;color:#555&#34;&gt;))&lt;/span&gt;.
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;Do you want to delete it? &lt;span style=&#34;color:#555&#34;&gt;[&lt;/span&gt;Y/n&lt;span style=&#34;color:#555&#34;&gt;]&lt;/span&gt; Y
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;error: failed to commit transaction &lt;span style=&#34;color:#555&#34;&gt;(&lt;/span&gt;invalid or corrupted package&lt;span style=&#34;color:#555&#34;&gt;)&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;Errors occurred, no packages were upgraded.
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;I followed advice &lt;a href=&#34;https://bbs.archlinux.org/viewtopic.php?id=158344&#34; target=&#34;_blank&#34; rel=&#34;noopener noreferrer&#34; &gt;in the forums&lt;/a&gt; and tried refreshing and repopulating the keys, clearing the Pacman cache, and a combination of these things. I still kept getting the same problem each time I tried to upgrade.&lt;/p&gt;
&lt;p&gt;Eventually I just removed the package, ran the upgrade, and then re-installed it:&lt;/p&gt;
&lt;div class=&#34;highlight&#34;&gt;&lt;pre tabindex=&#34;0&#34; style=&#34;background-color:#f0f3f3;-moz-tab-size:2;-o-tab-size:2;tab-size:2;&#34;&gt;&lt;code class=&#34;language-shell&#34; data-lang=&#34;shell&#34;&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;$ sudo pacman -R fail2ban
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;$ sudo pacman -Syyu
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;$ sudo pacman -S fail2ban
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;That fixed the problem, but I’m still not really sure why re-fetching the keys manually didn’t help!&lt;/p&gt;
</content:encoded>
    </item>
    
    <item>
      <title>Twitter Learnings</title>
      <link>https://wilw.dev/blog/2021/09/18/twitter-learnings/</link>
      <pubDate>Sat, 18 Sep 2021 10:04:00 +0000</pubDate>
      
      <guid>https://wilw.dev/blog/2021/09/18/twitter-learnings/</guid>
      
        <category>100daystooffload</category>
      
        <category>life</category>
      
      
      <content:encoded>&lt;p&gt;I recently &lt;a href=&#34;https://wilw.dev/blog/2021/09/07/addictive-twitter&#34;&gt;wrote about&lt;/a&gt; reviewing my Twitter usage, with the aim of discovering any constructive takeways I get from the platform that warrants me keeping it installed as an app on my phone.&lt;/p&gt;
&lt;p&gt;The up-shot is that I have now removed it. I didn’t delete my account as there is still enough value in visiting it less regularly (such as on my computer’s web-browser), but by removing the easy shortcut from my phone I have noticeably helped reduce the amount of time I spend &lt;a href=&#34;https://en.wikipedia.org/wiki/Doomscrolling&#34; target=&#34;_blank&#34; rel=&#34;noopener noreferrer&#34; &gt;doomscrolling&lt;/a&gt;.&lt;/p&gt;
&lt;p&gt;To reach this conclusion, I kept a sort of takeway &lt;em&gt;diary&lt;/em&gt;, in which I recorded useful or constructive things I learned or discovered whilst using the platform over the past week. The diary was nothing special - just a few ad-hoc bullet points in a note in &lt;a href=&#34;https://bear.app&#34; target=&#34;_blank&#34; rel=&#34;noopener noreferrer&#34; &gt;Bear&lt;/a&gt;. Some of the takeaways I’ll mention below.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Motivation&lt;/strong&gt; - both personal and professional. Twitter is full of inspirational stories about tech, business, and life. It’s mostly a bit showboat-y but can be good for motivation, but probably not useful (or healthy?) to read about every day since it (probably) wrongly implies these people have “perfect” lives.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Tech news&lt;/strong&gt; - most technologies I use (frameworks, tools, databases, etc.) have some sort of of Twitter presence, which can be useful for keeping up-to-date with advancements, updates, fixes, and more. This can be solved by adding these accounts to a Twitter list and checking-up on it manually on a periodic basis.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Humour&lt;/strong&gt; - probably the most addictive and enjoyable part about Twitter is the phatic content, that exists only for entertainment. Although this is generally good for the soul, it’s the thing that keeps me scrolling.&lt;/p&gt;
&lt;p&gt;Otherwise, general news is easy to find on Twitter (which can instead be replicated by following the right accounts on Mastodon or RSS feeds) and events coverage, such as the recent Apple event, which one can learn about by simply watching the event themselves.&lt;/p&gt;
&lt;p&gt;To me, the above doesn’t offset the negatives enough to keep me visiting on such a frequent basis, and it was time to break the addiction. The drama, complaining and - more and more - the witch-hunting that some people turn to Twitter for just always left a sour taste.&lt;/p&gt;
&lt;p&gt;&lt;a href=&#34;https://fosstodon.org/@pswilde&#34; target=&#34;_blank&#34; rel=&#34;noopener noreferrer&#34; &gt;@pswilde on Fosstodon&lt;/a&gt; helpfully pointed out the existence of &lt;a href=&#34;https://github.com/NicolasConstant/BirdsiteLive&#34; target=&#34;_blank&#34; rel=&#34;noopener noreferrer&#34; &gt;BirdsiteLive&lt;/a&gt; for bridging Twitter feeds into Mastodon, which I may use to keep up-to-date with a small number of Twitter accounts. But otherwise I am glad to be ready to control my usage a little better.&lt;/p&gt;
</content:encoded>
    </item>
    
    <item>
      <title>Columbus Day by Craig Alanson</title>
      <link>https://wilw.dev/blog/2021/09/12/columbus-day/</link>
      <pubDate>Sun, 12 Sep 2021 11:25:00 +0000</pubDate>
      
      <guid>https://wilw.dev/blog/2021/09/12/columbus-day/</guid>
      
        <category>100daystooffload</category>
      
        <category>book</category>
      
      
      <content:encoded>&lt;p&gt;Having recently read &lt;a href=&#34;https://wilw.dev/blog/2021/07/05/project-hail-mary&#34;&gt;Project Hail Mary&lt;/a&gt; - and rated it highly - Goodreads suggested I try &lt;a href=&#34;https://www.goodreads.com/book/show/36449535-columbus-day&#34; target=&#34;_blank&#34; rel=&#34;noopener noreferrer&#34; &gt;Columbus Day&lt;/a&gt; by &lt;a href=&#34;https://www.goodreads.com/author/show/14851776.Craig_Alanson&#34; target=&#34;_blank&#34; rel=&#34;noopener noreferrer&#34; &gt;Craig Alanson&lt;/a&gt;.&lt;/p&gt;
&lt;p&gt;&lt;img src=&#34;https://wilw.dev/media/blog/columbus_day.jpg&#34; alt=&#34;Columbus Day cover&#34;&gt;&lt;/p&gt;
&lt;p&gt;This is the first book in the &lt;a href=&#34;https://www.goodreads.com/series/185650-expeditionary-force&#34; target=&#34;_blank&#34; rel=&#34;noopener noreferrer&#34; &gt;Expeditionary Force&lt;/a&gt; series - one that I hadn’t yet heard of at the time.&lt;/p&gt;
&lt;p&gt;Although it’s sci-fi, the book is set in the modern-day. Earth gets invaded by far more technologically advanced aliens and humanity suddenly finds itself playing along as the lowest echelon in a war involving many different levels of alien capability. The mostly-powerless humans need to work out which side they should be fighting on.&lt;/p&gt;
&lt;p&gt;The book is humorous and full of comments from pop culture and classic “humanisms”, which often confuse the various non-human characters encountered. The story is told from the perspective of Joe Bishop - a former US Army soldier - who inadvertently finds himself pretty much at the centre of all that goes on. His character is a little rough around the edges but the reader quickly relates and warms to him.&lt;/p&gt;
&lt;p&gt;As far as the story goes, I found the first third or so to be a bit of a drag whilst the scene is set. However, it soon ramps up and it became very hard to put down once the main story began to kick-in.&lt;/p&gt;
&lt;p&gt;There’s a great mix of excitement, action, comedy, drama, and tragedy. It’s a book I can definitely recommend to those that enjoy action or more light-hearted sci-fi.&lt;/p&gt;
&lt;p&gt;The book ends on a note that paves a clear pathway to sequels and the rest of the series, and I look forward to reading them further!&lt;/p&gt;
</content:encoded>
    </item>
    
    <item>
      <title>Using Telegram bots to receive updates from your automated systems</title>
      <link>https://wilw.dev/blog/2021/09/09/telegram-notifications/</link>
      <pubDate>Thu, 09 Sep 2021 10:17:00 +0000</pubDate>
      
      <guid>https://wilw.dev/blog/2021/09/09/telegram-notifications/</guid>
      
        <category>100daystooffload</category>
      
        <category>technology</category>
      
      
      <content:encoded>&lt;h2 id=&#34;the-need-for-notifications&#34;&gt;The need for notifications&lt;/h2&gt;
&lt;p&gt;I self-host several services on various servers - for both some professional and personal uses.&lt;/p&gt;
&lt;p&gt;I use automated backup scripts to periodically sync data to Backblaze (which I &lt;a href=&#34;https://wilw.dev/blog/2021/05/18/b2-backups&#34;&gt;recently posted about&lt;/a&gt;). However, once they were setup I would often worry about whether they were working properly. To verify, I’d have to log into Backblaze and check when the latest backups came through.&lt;/p&gt;
&lt;p&gt;Although I trusted the process, this became a bit of a pain and more and more of a constant worry. The script might crash, run out of storage space, or anything else, and I wouldn’t know about it unless I actually checked.&lt;/p&gt;
&lt;p&gt;For peace of mind I wanted to be able to receive a notification each time the backups completed successfully.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Note: the same is true also for any automated system. For example, service crashes, key events, visitors, new registrations, etc. Such notifications are great for keeping in the loop without needing to actively check.&lt;/strong&gt;&lt;/p&gt;
&lt;h2 id=&#34;choosing-how-to-receive-notifications&#34;&gt;Choosing how to receive notifications&lt;/h2&gt;
&lt;p&gt;To start with, I needed to find a mechanism for &lt;em&gt;receiving&lt;/em&gt; the notifications. I looked around for suitable methods and noticed a number of commercial offerings (many with free tiers), such as &lt;a href=&#34;https://pushover.net&#34; target=&#34;_blank&#34; rel=&#34;noopener noreferrer&#34; &gt;Pushover&lt;/a&gt; and &lt;a href=&#34;https://healthchecks.io&#34; target=&#34;_blank&#34; rel=&#34;noopener noreferrer&#34; &gt;Healthchecks&lt;/a&gt;. Some of these required installation of new apps or weren’t quite right for my needs.&lt;/p&gt;
&lt;p&gt;Another option was to rely on Matrix, but although I have the &lt;a href=&#34;https://element.io&#34; target=&#34;_blank&#34; rel=&#34;noopener noreferrer&#34; &gt;Element&lt;/a&gt; app on my phone already I don’t use it a whole lot and I ideally just want to set something up for the long-term without needing to change it later.&lt;/p&gt;
&lt;p&gt;So I finally settled on Telegram. It’s an app I use every day on all of my devices anyway, and it would fit nicely into my existing workflows.&lt;/p&gt;
&lt;h2 id=&#34;using-telegram-to-receive-event-notifications&#34;&gt;Using Telegram to receive event notifications&lt;/h2&gt;
&lt;p&gt;Luckily, Telegram offers a great API around the notion of &lt;em&gt;bots&lt;/em&gt;. I created a simple bot that would send me a short message each time the backups completed. I’ll explain the process below, which should take less five minutes to complete.&lt;/p&gt;
&lt;h3 id=&#34;creating-the-bot&#34;&gt;Creating the bot&lt;/h3&gt;
&lt;p&gt;Telegram bots are managed through an existing bot, called the &lt;em&gt;BotFather&lt;/em&gt;. Begin a new chat with this bot by searching for it or by &lt;a href=&#34;https://t.me/botfather&#34; target=&#34;_blank&#34; rel=&#34;noopener noreferrer&#34; &gt;clicking this link directly&lt;/a&gt;.&lt;/p&gt;
&lt;p&gt;Once you’ve started the chat, type &lt;code&gt;/newbot&lt;/code&gt; to kickoff the bot-creation flow. BotFather will ask a few questions about your bot (e.g. a name and username), and will then create it for you. Given Telegram bots can be used by anyone, you’ll need a unique username for your bot.&lt;/p&gt;
&lt;p&gt;After the bot has been created, BotFather will give you an API token you can use to interact with your bot (i.e. read and send messages).&lt;/p&gt;
&lt;h3 id=&#34;creating-a-chat-with-your-new-bot&#34;&gt;Creating a chat with your new bot&lt;/h3&gt;
&lt;p&gt;The next stage is to begin a chat with your new bot. In Telegram, each chat has a unique ID, and we need to use this ID in order for your bot to send messages to the chat.&lt;/p&gt;
&lt;p&gt;First of all, open a chat with your new bot by either following the link to it from BotFather or by searching for its username. When the chat opens, tap &lt;code&gt;/start&lt;/code&gt; to begin the chat.&lt;/p&gt;
&lt;p&gt;Once you have initiated the conversation, the chat will be created. The next step is to retrieve the chat ID from the Telegram API.&lt;/p&gt;
&lt;p&gt;To do so, use cURL (or just your web browser) to make a &lt;code&gt;GET&lt;/code&gt; request to &lt;code&gt;https://api.telegram.org/bot&lt;API TOKEN&gt;/getUpdates&lt;/code&gt;, replacing &lt;code&gt;&lt;API TOKEN&gt;&lt;/code&gt; with the token provided earlier by BotFather.&lt;/p&gt;
&lt;p&gt;Some JSON will be displayed, describing the chats your bot is involved in. Given that no one knows about your bot yet, the results should contain just one entry, and will display your own username and details. Copy or make a note of the chat ID for your chat, as we’ll need that next.&lt;/p&gt;
&lt;h3 id=&#34;sending-notifications&#34;&gt;Sending notifications&lt;/h3&gt;
&lt;p&gt;The final part is now to use these details to send notifications from your bot. This is done by making a request to the following URL: &lt;code&gt;https://api.telegram.org/bot&lt;API TOKEN&gt;/sendMessage?chat_id=&lt;CHAT ID&gt;&amp;text=&lt;YOUR MESSAGE&gt;&lt;/code&gt; (again, replacing the bits in &lt;code&gt;&lt; &gt;&lt;/code&gt; with your own values).&lt;/p&gt;
&lt;p&gt;Your chat with the bot should receive the message and you’ll get a notification.&lt;/p&gt;
&lt;p&gt;You can now add a simple &lt;code&gt;curl&lt;/code&gt; command to the end of your important scripts, or embed a call to the URL in your applications to get updates about key events.&lt;/p&gt;
&lt;h3 id=&#34;customising-your-bot&#34;&gt;Customising your bot&lt;/h3&gt;
&lt;p&gt;You can also customise your bot with an avatar image and other details. To do so, head back over to your chat with BotFather and use the &lt;code&gt;/mybots&lt;/code&gt; command to get started.&lt;/p&gt;
&lt;h2 id=&#34;conclusion&#34;&gt;Conclusion&lt;/h2&gt;
&lt;p&gt;In this post I’ve briefly covered how to create a simple bot for providing event notifications.&lt;/p&gt;
&lt;p&gt;I hope you find it useful!&lt;/p&gt;
</content:encoded>
    </item>
    
    <item>
      <title>Twitter is still too addictive</title>
      <link>https://wilw.dev/blog/2021/09/07/addictive-twitter/</link>
      <pubDate>Tue, 07 Sep 2021 14:25:00 +0000</pubDate>
      
      <guid>https://wilw.dev/blog/2021/09/07/addictive-twitter/</guid>
      
        <category>100daystooffload</category>
      
        <category>life</category>
      
      
      <content:encoded>&lt;p&gt;&lt;strong&gt;TL,DR; I’m starting a Twitter diary to log interesting findings, and to measure its value to me.&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;Twitter is pretty much the last bastion of mainstream centralised social media that I use (aside from messaging services like Whatsapp and Telegram).&lt;/p&gt;
&lt;p&gt;Although I primarily use Mastodon for my every-day social networking, which is more focused on the things I am actually interested in, I always kept Twitter around too as an app on my phone. This is because every time I try to remove it, I quickly feel as though I must be missing out on &lt;em&gt;something&lt;/em&gt;. It always felt as though some news or useful announcement would go unnoticed.&lt;/p&gt;
&lt;p&gt;However, over the past few weeks and months I’ve found myself once again often getting into endless and mindless Twitter scrolling sessions. I feel as though I rarely come out of these with any positive gain, and then get frustrated with myself for wasting time.&lt;/p&gt;
&lt;p&gt;One might ask what is is that I get out of Mastodon, and why this is time more usefully spent than on Twitter. This equally then may lead to thoughts along the lines of &lt;em&gt;“you’re just not following the right people”&lt;/em&gt;. This is a fair comment, but curating a following list should be an interesting and pleasurable experience. This feels to be no longer the case on Twitter.&lt;/p&gt;
&lt;p&gt;Beyond this, there’s also the notion of the algorithmic timeline (tweets you may have missed, or which Twitter thinks you should see again), as well as the pseudo timelines you’re force-fed around the “likes” of those you follow.&lt;/p&gt;
&lt;p&gt;This is seemingly at random, but I imagine it is massively under the control of “the Twitter &lt;em&gt;algorithm&lt;/em&gt;” to the extent where what was once an innocent and useful bookmarking feature now becomes a game of retweet roulette.&lt;/p&gt;
&lt;p&gt;More and more I even see cases of tweets injected into my timeline from users that are followed by the people &lt;em&gt;I&lt;/em&gt; follow  - this is two hops through the social graph but without explicit retweets or likes from those that I directly follow. Given Twitter’s own &lt;a href=&#34;https://www.aaai.org/ocs/index.php/SOCS/SOCS11/paper/view/4031&#34; target=&#34;_blank&#34; rel=&#34;noopener noreferrer&#34; &gt;3.4 degrees of separation&lt;/a&gt; (and that paper is from 10 years ago already), what is the point of choosing who to follow?&lt;/p&gt;
&lt;p&gt;On top of these “features” are the seemingly-newer “topics you may like” feature and, of course, the adverts.&lt;/p&gt;
&lt;p&gt;The image below shows my Twitter home feed containing three tweets (the first and last cannot be fully shown due to the size of my phone screen). None of these tweets are from people I follow; the first is an ad, the second is advertising a “topic”, and the last is a “like” from someone I do follow.&lt;/p&gt;
&lt;p&gt;&lt;img src=&#34;https://wilw.dev/media/blog/addictive_twitter_1.png&#34; alt=&#34;Twitter screenshot showing my homefeed containing three tweets from people I don&amp;rsquo;t follow&#34;&gt;&lt;/p&gt;
&lt;p&gt;All of this boils down to the fact that choosing who you follow becomes less and less impactful on the information you actually receive on Twitter. The only reason for this is that it makes the platform more addictive (as I’ve experienced) &lt;em&gt;so that&lt;/em&gt; you spend more time on it &lt;em&gt;so that&lt;/em&gt; Twitter makes more money from you viewing the ads. Although there are other apps available that try to bypass some of these “features”, it doesn’t feel right to give the service more of my time without getting anything useful out of it.&lt;/p&gt;
&lt;h2 id=&#34;anyway-rant-over-what-am-i-going-to-do-about-it&#34;&gt;Anyway, /rant over. What am I going to do about it?&lt;/h2&gt;
&lt;p&gt;It’s time to qualify what Twitter actually &lt;em&gt;does&lt;/em&gt; offer. I’ve started a sort of Twitter “diary”, in which I plan to log takeaways from my time on the platform. If I find myself on Twitter (as I often subconciously do, even having removed it from my homescreen), I will note useful or meaningful learnings or findings in the diary. I’ll also include humorous and phatic content, since a laugh always feels constructive (to me, anyway).&lt;/p&gt;
&lt;p&gt;After a few weeks, I’ll assess the diary and measure any learnings against whether I could have learned the same from other sources (e.g. my RSS feeds or Mastodon).&lt;/p&gt;
</content:encoded>
    </item>
    
    <item>
      <title>SSO Tools</title>
      <link>https://wilw.dev/blog/2021/09/02/sso-tools/</link>
      <pubDate>Thu, 02 Sep 2021 19:58:00 +0000</pubDate>
      
      <guid>https://wilw.dev/blog/2021/09/02/sso-tools/</guid>
      
        <category>100daystooffload</category>
      
        <category>technology</category>
      
        <category>project</category>
      
      
      <content:encoded>&lt;p&gt;Another &lt;a href=&#34;https://wilw.dev/projects&#34;&gt;project&lt;/a&gt; I try to maintain (when I can!) is &lt;a href=&#34;https://sso.tools&#34; target=&#34;_blank&#34; rel=&#34;noopener noreferrer&#34; &gt;SSO Tools&lt;/a&gt;.&lt;/p&gt;
&lt;p&gt;This is a simple web service that aims to help developers test their own services’ single sign-on (SSO) functionality. The motivation behind the project was that many commmercial offerings were too expensive for solo developers, or just far too complex for simple testing.&lt;/p&gt;
&lt;p&gt;SSO Tools aims to provide a simple interface, with functionality that allows for registering identity providers (IdPs), test IdP users, and service providers (SPs). It is targeted at developers looking to quickly, yet robustly, test and iterate on their SSO setup in their applications.&lt;/p&gt;
&lt;p&gt;&lt;img src=&#34;https://wilw.dev/media/blog/sso_tools_1.png&#34; alt=&#34;The IdP users interface for managing user attributes&#34;&gt;&lt;/p&gt;
&lt;p&gt;The IdP users can be configured with custom attributes to allow for observing how a service provider application behaves when receiving different attributes.&lt;/p&gt;
&lt;p&gt;Currently SSO Tools allows for testing SAML2 configurations, and supports both SP-initiated and IdP-initiated login for SSO. It also supports single-logout functionality, in which the user is automatically logged-out of the IdP when the user logs out of a connected service.&lt;/p&gt;
&lt;p&gt;&lt;img src=&#34;https://wilw.dev/media/blog/sso_tools_2.png&#34; alt=&#34;Viewing SAML2 setup details&#34;&gt;&lt;/p&gt;
&lt;p&gt;SSO Tools is offered as a free service. I needed something myself when building out a SAML2 service provider, and thought the features could be useful for others too.&lt;/p&gt;
&lt;p&gt;If you’d like to help or get involved in the project then I’d love to hear from you.&lt;/p&gt;
</content:encoded>
    </item>
    
    <item>
      <title>Darknet Diaries: True stories from the dark side of the Internet</title>
      <link>https://wilw.dev/blog/2021/08/29/darknet-diaries/</link>
      <pubDate>Sun, 29 Aug 2021 12:11:00 +0000</pubDate>
      
      <guid>https://wilw.dev/blog/2021/08/29/darknet-diaries/</guid>
      
        <category>100daystooffload</category>
      
        <category>podcast</category>
      
      
      <content:encoded>&lt;p&gt;Another of my &lt;a href=&#34;https://wilw.dev/tags/podcast&#34;&gt;favourite podcasts&lt;/a&gt; is &lt;a href=&#34;https://darknetdiaries.com&#34; target=&#34;_blank&#34; rel=&#34;noopener noreferrer&#34; &gt;Darknet Diaries&lt;/a&gt;.&lt;/p&gt;
&lt;p&gt;&lt;img src=&#34;https://wilw.dev/media/blog/darknet_diaries.jpg&#34; alt=&#34;Darknet Diaries cover art&#34;&gt;&lt;/p&gt;
&lt;p&gt;Created and presented by the excellent Jack Rhysider, Darknet Diaries releases new episodes fornightly. Each episode contains a true story from the “dark side of the internet” and includes content related to cybercrime, hacking (in the information security sense), dodgy government activity, and much more.&lt;/p&gt;
&lt;p&gt;Typically, most episodes involve guests that Jack interviews in order to tell a story. Although it does occasionally stray into some technical detail, the vast majority of episodes are totally accessible to everyone.&lt;/p&gt;
&lt;p&gt;If you have an interest in tech and love stories about crime or beneath-the-radar operations, then this podcast is perfect for you. I always look forward to new episodes.&lt;/p&gt;
&lt;p&gt;Whilst some stories may be shocking (or even occasionally a little disturbing), they are all certainly exciting and super interesting. I’ve learned a lot and I can also definitely recommend listening if you’re involved in security or operations in your own work.&lt;/p&gt;
&lt;p&gt;If you’re interested in giving it a go, you can &lt;a href=&#34;https://darknetdiaries.com/subscribe&#34; target=&#34;_blank&#34; rel=&#34;noopener noreferrer&#34; &gt;subscribe to new episodes&lt;/a&gt;. If you end up enjoying it, then you can also &lt;a href=&#34;https://darknetdiaries.com/donate&#34; target=&#34;_blank&#34; rel=&#34;noopener noreferrer&#34; &gt;donate to the show&lt;/a&gt;.&lt;/p&gt;
&lt;p&gt;I hope you enjoy!&lt;/p&gt;
</content:encoded>
    </item>
    
    <item>
      <title>Stripping sensitive EXIF data from uploaded images</title>
      <link>https://wilw.dev/blog/2021/08/28/stripping-exif/</link>
      <pubDate>Sat, 28 Aug 2021 16:46:00 +0000</pubDate>
      
      <guid>https://wilw.dev/blog/2021/08/28/stripping-exif/</guid>
      
        <category>100daystooffload</category>
      
        <category>technology</category>
      
        <category>python</category>
      
      
        <enclosure url="https://wilw.dev/media/blog/header-stripping-exif.png" type="image/png"/>
      
      <content:encoded>&lt;h2 id=&#34;the-problem-with-image-uploads&#34;&gt;The problem with image uploads&lt;/h2&gt;
&lt;p&gt;Many services - including web and mobile apps - allow for their users to upload imagery. This could be to enable users to upload an avatar image or perhaps create a gallery of image files.&lt;/p&gt;
&lt;p&gt;Either way, many photos contain some degree of sensitive metadata information as part of their &lt;em&gt;EXIF data&lt;/em&gt;. For example, if you take photos using your phone, it is likely that the camera application will embed metadata into the image file it creates. This could include the geocoordinates of the position from where the photo was taken, the make and model of the camera device, as well as lots of other data (exposure time, focus, balances, etc).&lt;/p&gt;
&lt;p&gt;This data is useful, since it allows photo-viewing applications to display your images on a map or to allow for examining files in more detail.&lt;/p&gt;
&lt;p&gt;However, if a user chooses to upload such an image file to a service, unless the metadata is correctly stripped out the &lt;em&gt;exact&lt;/em&gt; same file can be downloaded by other users - which could give them access to sensitive information. For example, if someone created an image post along with some text describing themselves as being at home, other users could quickly (and accurately) discover where the user lives.&lt;/p&gt;
&lt;p&gt;In this post I will cover:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;A brief introduction to the Pillow image processing library&lt;/li&gt;
&lt;li&gt;How to use Python to process images in order to remove EXIF data&lt;/li&gt;
&lt;li&gt;How the approach can be automated when using S3 and AWS Lambda&lt;/li&gt;
&lt;/ul&gt;
&lt;h2 id=&#34;a-basic-approach-to-removing-sensitive-information&#34;&gt;A basic approach to removing sensitive information&lt;/h2&gt;
&lt;p&gt;Luckily, removing sensitive EXIF data is not too difficult to do. This post leverages Python for demonstrating the approach, however the process would be very similar in other languages too. As such, the discussed method could easily be included as part of a Flask or Django service, or a standalone Python application.&lt;/p&gt;
&lt;p&gt;Specifically, I will make use of the &lt;a href=&#34;https://pillow.readthedocs.io/en/stable&#34; target=&#34;_blank&#34; rel=&#34;noopener noreferrer&#34; &gt;Pillow&lt;/a&gt; library for Python - it is a very useful tool for manipulating images.&lt;/p&gt;
&lt;p&gt;This post does not cover the process or code required to actually upload a file from a client, since the following approach is agnostic as to whether the service is part of a mobile app, a web app, a simple API, a local application, or anything else.&lt;/p&gt;
&lt;p&gt;Another thing to remember is that orientation data (i.e. the positioning of the camera when the photo was taken) is also included in the EXIF data. This information tells image viewers with which rotation to display the image, and so we will need to handle this too.&lt;/p&gt;
&lt;p&gt;The following provides a basic approach to accomplishing what we are trying to achieve.&lt;/p&gt;
&lt;p&gt;First, install the needed dependency:&lt;/p&gt;
&lt;div class=&#34;highlight&#34;&gt;&lt;pre tabindex=&#34;0&#34; style=&#34;background-color:#f0f3f3;-moz-tab-size:2;-o-tab-size:2;tab-size:2;&#34;&gt;&lt;code class=&#34;language-shell&#34; data-lang=&#34;shell&#34;&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;poetry add pillow
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;Or use &lt;code&gt;pip&lt;/code&gt; if preferred.&lt;/p&gt;
&lt;p&gt;You’re then ready to write your code, assuming the Python application has a reference to the image data (e.g. by reading from a request stream, a file, or elsewhere) called &lt;code&gt;image_data&lt;/code&gt;.&lt;/p&gt;
&lt;div class=&#34;highlight&#34;&gt;&lt;pre tabindex=&#34;0&#34; style=&#34;background-color:#f0f3f3;-moz-tab-size:2;-o-tab-size:2;tab-size:2;&#34;&gt;&lt;code class=&#34;language-python&#34; data-lang=&#34;python&#34;&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;&lt;span style=&#34;color:#09f;font-style:italic&#34;&gt;# At the top of the file, include PIL in your imports:&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;&lt;span style=&#34;color:#069;font-weight:bold&#34;&gt;from&lt;/span&gt; &lt;span style=&#34;color:#0cf;font-weight:bold&#34;&gt;PIL&lt;/span&gt; &lt;span style=&#34;color:#069;font-weight:bold&#34;&gt;import&lt;/span&gt; Image, ExifTags
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;&lt;span style=&#34;color:#069;font-weight:bold&#34;&gt;import&lt;/span&gt; &lt;span style=&#34;color:#0cf;font-weight:bold&#34;&gt;io&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;&lt;span style=&#34;color:#555&#34;&gt;...&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;&lt;span style=&#34;color:#09f;font-style:italic&#34;&gt;# Calculate orientation key in ExifTags&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;orientation &lt;span style=&#34;color:#555&#34;&gt;=&lt;/span&gt; &lt;span style=&#34;color:#069;font-weight:bold&#34;&gt;None&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;&lt;span style=&#34;color:#069;font-weight:bold&#34;&gt;for&lt;/span&gt; orientation &lt;span style=&#34;color:#000;font-weight:bold&#34;&gt;in&lt;/span&gt; ExifTags&lt;span style=&#34;color:#555&#34;&gt;.&lt;/span&gt;TAGS&lt;span style=&#34;color:#555&#34;&gt;.&lt;/span&gt;keys():
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;  &lt;span style=&#34;color:#069;font-weight:bold&#34;&gt;if&lt;/span&gt; ExifTags&lt;span style=&#34;color:#555&#34;&gt;.&lt;/span&gt;TAGS[orientation] &lt;span style=&#34;color:#555&#34;&gt;==&lt;/span&gt; &lt;span style=&#34;color:#c30&#34;&gt;&#39;Orientation&#39;&lt;/span&gt;:
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;    &lt;span style=&#34;color:#069;font-weight:bold&#34;&gt;break&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;&lt;span style=&#34;color:#09f;font-style:italic&#34;&gt;# Create a new PIL image from the image_data:&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;image &lt;span style=&#34;color:#555&#34;&gt;=&lt;/span&gt; Image&lt;span style=&#34;color:#555&#34;&gt;.&lt;/span&gt;open(image_data)
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;&lt;span style=&#34;color:#09f;font-style:italic&#34;&gt;# We need to use the orientation EXIF info to rotate the image:&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;image_exif &lt;span style=&#34;color:#555&#34;&gt;=&lt;/span&gt; image&lt;span style=&#34;color:#555&#34;&gt;.&lt;/span&gt;getexif()
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;&lt;span style=&#34;color:#069;font-weight:bold&#34;&gt;if&lt;/span&gt; image_exif:
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;  exif &lt;span style=&#34;color:#555&#34;&gt;=&lt;/span&gt; &lt;span style=&#34;color:#366&#34;&gt;dict&lt;/span&gt;(image_exif&lt;span style=&#34;color:#555&#34;&gt;.&lt;/span&gt;items())
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;  &lt;span style=&#34;color:#069;font-weight:bold&#34;&gt;if&lt;/span&gt; exif&lt;span style=&#34;color:#555&#34;&gt;.&lt;/span&gt;get(orientation) &lt;span style=&#34;color:#555&#34;&gt;==&lt;/span&gt; &lt;span style=&#34;color:#f60&#34;&gt;3&lt;/span&gt;:
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;    image &lt;span style=&#34;color:#555&#34;&gt;=&lt;/span&gt; image&lt;span style=&#34;color:#555&#34;&gt;.&lt;/span&gt;rotate(&lt;span style=&#34;color:#f60&#34;&gt;180&lt;/span&gt;, expand&lt;span style=&#34;color:#555&#34;&gt;=&lt;/span&gt;&lt;span style=&#34;color:#069;font-weight:bold&#34;&gt;True&lt;/span&gt;)
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;  &lt;span style=&#34;color:#069;font-weight:bold&#34;&gt;elif&lt;/span&gt; exif&lt;span style=&#34;color:#555&#34;&gt;.&lt;/span&gt;get(orientation) &lt;span style=&#34;color:#555&#34;&gt;==&lt;/span&gt; &lt;span style=&#34;color:#f60&#34;&gt;6&lt;/span&gt;:
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;    image &lt;span style=&#34;color:#555&#34;&gt;=&lt;/span&gt; image&lt;span style=&#34;color:#555&#34;&gt;.&lt;/span&gt;rotate(&lt;span style=&#34;color:#f60&#34;&gt;270&lt;/span&gt;, expand&lt;span style=&#34;color:#555&#34;&gt;=&lt;/span&gt;&lt;span style=&#34;color:#069;font-weight:bold&#34;&gt;True&lt;/span&gt;)
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;  &lt;span style=&#34;color:#069;font-weight:bold&#34;&gt;elif&lt;/span&gt; exif&lt;span style=&#34;color:#555&#34;&gt;.&lt;/span&gt;get(orientation) &lt;span style=&#34;color:#555&#34;&gt;==&lt;/span&gt; &lt;span style=&#34;color:#f60&#34;&gt;8&lt;/span&gt;:
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;    image &lt;span style=&#34;color:#555&#34;&gt;=&lt;/span&gt; image&lt;span style=&#34;color:#555&#34;&gt;.&lt;/span&gt;rotate(&lt;span style=&#34;color:#f60&#34;&gt;90&lt;/span&gt;, expand&lt;span style=&#34;color:#555&#34;&gt;=&lt;/span&gt;&lt;span style=&#34;color:#069;font-weight:bold&#34;&gt;True&lt;/span&gt;)
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;&lt;span style=&#34;color:#09f;font-style:italic&#34;&gt;# Retrieve the image contents as a list representing a sequence of pixel values:&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;image_contents &lt;span style=&#34;color:#555&#34;&gt;=&lt;/span&gt; &lt;span style=&#34;color:#366&#34;&gt;list&lt;/span&gt;(image&lt;span style=&#34;color:#555&#34;&gt;.&lt;/span&gt;getdata())
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;&lt;span style=&#34;color:#09f;font-style:italic&#34;&gt;# Create a new image based on the original, but without the full EXIF data:&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;new_image &lt;span style=&#34;color:#555&#34;&gt;=&lt;/span&gt; Image&lt;span style=&#34;color:#555&#34;&gt;.&lt;/span&gt;new(image&lt;span style=&#34;color:#555&#34;&gt;.&lt;/span&gt;mode, image&lt;span style=&#34;color:#555&#34;&gt;.&lt;/span&gt;size)
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;new_image&lt;span style=&#34;color:#555&#34;&gt;.&lt;/span&gt;putdata(image_contents)
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;&lt;span style=&#34;color:#09f;font-style:italic&#34;&gt;# Finally, create a new buffer object and put the new image file data into it:&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;new_image_data &lt;span style=&#34;color:#555&#34;&gt;=&lt;/span&gt; io&lt;span style=&#34;color:#555&#34;&gt;.&lt;/span&gt;BytesIO()
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;new_image&lt;span style=&#34;color:#555&#34;&gt;.&lt;/span&gt;save(new_image_data, &lt;span style=&#34;color:#c30&#34;&gt;&#39;PNG&#39;&lt;/span&gt;) &lt;span style=&#34;color:#09f;font-style:italic&#34;&gt;# Or the file format needed&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;new_image_data&lt;span style=&#34;color:#555&#34;&gt;.&lt;/span&gt;seek(&lt;span style=&#34;color:#f60&#34;&gt;0&lt;/span&gt;)
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;And that’s it. The &lt;code&gt;new_image_data&lt;/code&gt; object can now be sent to wherever you need it to be - the filesystem, another server somewhere, or to a storage solution like S3. For example, you may want to give the new image the same filename such that it overwrites the original.&lt;/p&gt;
&lt;p&gt;During this process you can also leverage Pillow to do some additional image processing, such as resizing, cropping, colour transformations, or even statistical analyses. I recommend checking out the &lt;a href=&#34;https://pillow.readthedocs.io/en/stable/handbook/index.html&#34; target=&#34;_blank&#34; rel=&#34;noopener noreferrer&#34; &gt;handbook&lt;/a&gt; for more information on this.&lt;/p&gt;
&lt;h2 id=&#34;working-with-amazon-s3&#34;&gt;Working with Amazon S3&lt;/h2&gt;
&lt;p&gt;If you use S3 as a storage backend, you can easily add an S3 trigger - along with a quick Lambda function - to automatically handle this process for you without the need for additional servers or changes to your existing application.&lt;/p&gt;
&lt;p&gt;This process will cause S3 to automatically trigger your Lambda function each time a new file is uploaded to your bucket.&lt;/p&gt;
&lt;p&gt;To start with, bundle the above code into a function called &lt;code&gt;handler&lt;/code&gt; that accepts an &lt;code&gt;event&lt;/code&gt; object as an argument. You’ll also need to import the &lt;code&gt;boto3&lt;/code&gt; library:&lt;/p&gt;
&lt;div class=&#34;highlight&#34;&gt;&lt;pre tabindex=&#34;0&#34; style=&#34;background-color:#f0f3f3;-moz-tab-size:2;-o-tab-size:2;tab-size:2;&#34;&gt;&lt;code class=&#34;language-python&#34; data-lang=&#34;python&#34;&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;&lt;span style=&#34;color:#09f;font-style:italic&#34;&gt;# Include the previous imports from above...&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;&lt;span style=&#34;color:#09f;font-style:italic&#34;&gt;# Also import boto3 and os:&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;&lt;span style=&#34;color:#069;font-weight:bold&#34;&gt;import&lt;/span&gt; &lt;span style=&#34;color:#0cf;font-weight:bold&#34;&gt;boto3&lt;/span&gt;&lt;span style=&#34;color:#555&#34;&gt;,&lt;/span&gt; &lt;span style=&#34;color:#0cf;font-weight:bold&#34;&gt;os&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;&lt;span style=&#34;color:#09f;font-style:italic&#34;&gt;# Create a new function&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;&lt;span style=&#34;color:#069;font-weight:bold&#34;&gt;def&lt;/span&gt; &lt;span style=&#34;color:#c0f&#34;&gt;handler&lt;/span&gt;(event):
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;  &lt;span style=&#34;color:#09f;font-style:italic&#34;&gt;# Create a new S3 client:&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;  s3 &lt;span style=&#34;color:#555&#34;&gt;=&lt;/span&gt; boto3&lt;span style=&#34;color:#555&#34;&gt;.&lt;/span&gt;client(&lt;span style=&#34;color:#c30&#34;&gt;&#39;s3&#39;&lt;/span&gt;)
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;  &lt;span style=&#34;color:#09f;font-style:italic&#34;&gt;# Get the pathname to the uploaded object from the event:&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;  file_path &lt;span style=&#34;color:#555&#34;&gt;=&lt;/span&gt; event[&lt;span style=&#34;color:#c30&#34;&gt;&#39;Records&#39;&lt;/span&gt;][&lt;span style=&#34;color:#f60&#34;&gt;0&lt;/span&gt;][&lt;span style=&#34;color:#c30&#34;&gt;&#39;s3&#39;&lt;/span&gt;][&lt;span style=&#34;color:#c30&#34;&gt;&#39;object&#39;&lt;/span&gt;][&lt;span style=&#34;color:#c30&#34;&gt;&#39;key&#39;&lt;/span&gt;]
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;  &lt;span style=&#34;color:#09f;font-style:italic&#34;&gt;# Extract the file name and extension&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;  file_name, file_extension &lt;span style=&#34;color:#555&#34;&gt;=&lt;/span&gt; os&lt;span style=&#34;color:#555&#34;&gt;.&lt;/span&gt;path&lt;span style=&#34;color:#555&#34;&gt;.&lt;/span&gt;splitext(file_path)
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;  &lt;span style=&#34;color:#09f;font-style:italic&#34;&gt;# Check the file is an image (and not another type of file, like a PDF):&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;  &lt;span style=&#34;color:#069;font-weight:bold&#34;&gt;if&lt;/span&gt; file_extension&lt;span style=&#34;color:#555&#34;&gt;.&lt;/span&gt;lower() &lt;span style=&#34;color:#000;font-weight:bold&#34;&gt;not&lt;/span&gt; &lt;span style=&#34;color:#000;font-weight:bold&#34;&gt;in&lt;/span&gt; [&lt;span style=&#34;color:#c30&#34;&gt;&#39;.jpg&#39;&lt;/span&gt;, &lt;span style=&#34;color:#c30&#34;&gt;&#39;.png&#39;&lt;/span&gt;, &lt;span style=&#34;color:#c30&#34;&gt;&#39;.jpeg&#39;&lt;/span&gt;]:
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;    &lt;span style=&#34;color:#09f;font-style:italic&#34;&gt;# If not an image, bail out early:&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;    &lt;span style=&#34;color:#069;font-weight:bold&#34;&gt;return&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;  &lt;span style=&#34;color:#09f;font-style:italic&#34;&gt;# Retrieve the object&#39;s tags. Here we can check if the file&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;  &lt;span style=&#34;color:#09f;font-style:italic&#34;&gt;# has already been processed in order to prevent an infinite loop:&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;  tagging &lt;span style=&#34;color:#555&#34;&gt;=&lt;/span&gt; s3&lt;span style=&#34;color:#555&#34;&gt;.&lt;/span&gt;get_object_tagging(Bucket&lt;span style=&#34;color:#555&#34;&gt;=&lt;/span&gt;&lt;span style=&#34;color:#c30&#34;&gt;&#39;mybucket&#39;&lt;/span&gt;, Key&lt;span style=&#34;color:#555&#34;&gt;=&lt;/span&gt;file_path)
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;  object_tags &lt;span style=&#34;color:#555&#34;&gt;=&lt;/span&gt; tagging[&lt;span style=&#34;color:#c30&#34;&gt;&#39;TagSet&#39;&lt;/span&gt;]
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;  &lt;span style=&#34;color:#09f;font-style:italic&#34;&gt;# Go through the tags and exit if EXIF data has already been stripped:&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;  &lt;span style=&#34;color:#069;font-weight:bold&#34;&gt;for&lt;/span&gt; tag &lt;span style=&#34;color:#000;font-weight:bold&#34;&gt;in&lt;/span&gt; object_tags:
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;    &lt;span style=&#34;color:#069;font-weight:bold&#34;&gt;if&lt;/span&gt; tag[&lt;span style=&#34;color:#c30&#34;&gt;&#39;Key&#39;&lt;/span&gt;] &lt;span style=&#34;color:#555&#34;&gt;==&lt;/span&gt; &lt;span style=&#34;color:#c30&#34;&gt;&#39;ExifStripped&#39;&lt;/span&gt; &lt;span style=&#34;color:#000;font-weight:bold&#34;&gt;and&lt;/span&gt; tag[&lt;span style=&#34;color:#c30&#34;&gt;&#39;Value&#39;&lt;/span&gt;] &lt;span style=&#34;color:#555&#34;&gt;==&lt;/span&gt; &lt;span style=&#34;color:#c30&#34;&gt;&#39;True&#39;&lt;/span&gt;:
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;      &lt;span style=&#34;color:#069;font-weight:bold&#34;&gt;return&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;  &lt;span style=&#34;color:#09f;font-style:italic&#34;&gt;# Retrieve the image from S3 into the image_data object:&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;  image_data &lt;span style=&#34;color:#555&#34;&gt;=&lt;/span&gt; s3&lt;span style=&#34;color:#555&#34;&gt;.&lt;/span&gt;get_object(Bucket&lt;span style=&#34;color:#555&#34;&gt;=&lt;/span&gt;&lt;span style=&#34;color:#c30&#34;&gt;&#39;mybucket&#39;&lt;/span&gt;, Key&lt;span style=&#34;color:#555&#34;&gt;=&lt;/span&gt;file_path)
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;  &lt;span style=&#34;color:#09f;font-style:italic&#34;&gt;# Now process the file by including the earlier code&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;  &lt;span style=&#34;color:#555&#34;&gt;...&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;  &lt;span style=&#34;color:#555&#34;&gt;...&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;  &lt;span style=&#34;color:#09f;font-style:italic&#34;&gt;# Finally overwrite the existing S3 object with the new image file data and tag:&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;  s3&lt;span style=&#34;color:#555&#34;&gt;.&lt;/span&gt;put_object(Bucket&lt;span style=&#34;color:#555&#34;&gt;=&lt;/span&gt;&lt;span style=&#34;color:#c30&#34;&gt;&#39;mybucket&#39;&lt;/span&gt;, Key&lt;span style=&#34;color:#555&#34;&gt;=&lt;/span&gt;file_path, Body&lt;span style=&#34;color:#555&#34;&gt;=&lt;/span&gt;new_image_data, Tagging&lt;span style=&#34;color:#555&#34;&gt;=&lt;/span&gt;&lt;span style=&#34;color:#c30&#34;&gt;&#39;ExifStripped=True&#39;&lt;/span&gt;)
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;Replace &lt;code&gt;mybucket&lt;/code&gt; with the name of your existing S3 bucket. Depending on your setup you may need to add additional configuration to the S3 client (such as the region of your bucket).&lt;/p&gt;
&lt;p&gt;We use &lt;a href=&#34;https://docs.aws.amazon.com/AmazonS3/latest/userguide/object-tagging.html&#34; target=&#34;_blank&#34; rel=&#34;noopener noreferrer&#34; &gt;S3 object tagging&lt;/a&gt; to tag files we’ve already processed. Otherwise the final &lt;code&gt;put_object&lt;/code&gt; call would result in S3 recursively calling the function again and again.&lt;/p&gt;
&lt;p&gt;Next, deploy the above function to AWS Lambda - either through the command line or by copying the code into the AWS web interface. You’ll need to name your function appropriately (e.g. &lt;code&gt;image_handler&lt;/code&gt;) and remember to register the &lt;code&gt;handler&lt;/code&gt; function as the Lambda handler.&lt;/p&gt;
&lt;p&gt;You should assign a suitable IAM role that allows the function to read and write from and to your S3 bucket, and also to read and write tags. I won’t cover this here, since if you already use S3 I’ll assume you’re also familiar with IAM.&lt;/p&gt;
&lt;p&gt;To create the trigger in the S3 console, navigate to your bucket’s “Properties” tab and click the “Create event notification” button. Go through the form and configure the trigger based on your needs, including your new Lambda function in the “Destination” section.&lt;/p&gt;
&lt;p&gt;&lt;img src=&#34;https://wilw.dev/media/blog/exif_strip1.png&#34; alt=&#34;Configuring the S3 event trigger&#34;&gt;&lt;/p&gt;
&lt;p&gt;Configure the basic details as above, replacing the information as needed.&lt;/p&gt;
&lt;p&gt;&lt;img src=&#34;https://wilw.dev/media/blog/exif_strip2.png&#34; alt=&#34;Set up the Lambda function as the destination&#34;&gt;&lt;/p&gt;
&lt;p&gt;Finally, choose your Lambda function from the list as a destination, as shown above.&lt;/p&gt;
&lt;h2 id=&#34;conclusion&#34;&gt;Conclusion&lt;/h2&gt;
&lt;p&gt;In this post I have talked about one method for handling uploaded images in order to remove sensitive EXIF data and also explained how the same process can be used to incorporate other types of image processing.&lt;/p&gt;
&lt;p&gt;Whilst the approach won’t be suitable for everyone or for all use cases, it should provide additional means for protecting the security of users of your services and applications.&lt;/p&gt;
&lt;h2 id=&#34;postscript&#34;&gt;Postscript&lt;/h2&gt;
&lt;p&gt;Update (February 2023): Thanks to &lt;a href=&#34;https://www.twitter.com/flightlesstux&#34; target=&#34;_blank&#34; rel=&#34;noopener noreferrer&#34; &gt;Ercan&lt;/a&gt; for pointing out an omission in this blog post relating to calculating the &lt;code&gt;orientation&lt;/code&gt; key.&lt;/p&gt;
</content:encoded>
    </item>
    
    <item>
      <title>Adding &#39;dark mode&#39; and dynamic theming to your React websites</title>
      <link>https://wilw.dev/blog/2021/08/21/react-theming/</link>
      <pubDate>Sat, 21 Aug 2021 13:05:00 +0000</pubDate>
      
      <guid>https://wilw.dev/blog/2021/08/21/react-theming/</guid>
      
        <category>100daystooffload</category>
      
        <category>technology</category>
      
        <category>javascript</category>
      
        <category>react</category>
      
      
        <enclosure url="https://wilw.dev/media/blog/header-react-theming.png" type="image/png"/>
      
      <content:encoded>&lt;p&gt;Adding theming and the choice between “light” and “dark” modes to your website can enhance your site’s accessibility and make it feel more consistent with the host operating system’s own theme.&lt;/p&gt;
&lt;p&gt;With JavaScript (and React in particular) this is easy to do, as I’ll explain in this post. We’ll use a combination of our own JavaScript code, the &lt;a href=&#34;https://github.com/pmndrs/zustand&#34; target=&#34;_blank&#34; rel=&#34;noopener noreferrer&#34; &gt;Zustand state management library&lt;/a&gt;, and the &lt;a href=&#34;https://styled-components.com&#34; target=&#34;_blank&#34; rel=&#34;noopener noreferrer&#34; &gt;styled components&lt;/a&gt; package to achieve the following:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;Create a theme library&lt;/li&gt;
&lt;li&gt;Dynamically update the site’s styles based on the theme&lt;/li&gt;
&lt;li&gt;Allow the user to change the theme&lt;/li&gt;
&lt;li&gt;Remember the theme for the next time the user visits&lt;/li&gt;
&lt;li&gt;Detect the operating system dark/light mode to auto-set an appropriate theme&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;This post is written from the perspective of a GatsbyJS website, however the same concepts should apply to any React single-page application.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;TL;DR: If you just want the code, scroll to the ‘Complete code’ section at the bottom of the post.&lt;/strong&gt;&lt;/p&gt;
&lt;h2 id=&#34;setting-up-the-theming&#34;&gt;Setting up the theming&lt;/h2&gt;
&lt;p&gt;To start, add these new packages to the project:&lt;/p&gt;
&lt;div class=&#34;highlight&#34;&gt;&lt;pre tabindex=&#34;0&#34; style=&#34;background-color:#f0f3f3;-moz-tab-size:2;-o-tab-size:2;tab-size:2;&#34;&gt;&lt;code class=&#34;language-shell&#34; data-lang=&#34;shell&#34;&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;yarn add zustand styled-components
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;Next, declare some themes! For this you can create a &lt;code&gt;themes.js&lt;/code&gt; file (e.g. in your top level &lt;code&gt;src/&lt;/code&gt; directory):&lt;/p&gt;
&lt;div class=&#34;highlight&#34;&gt;&lt;pre tabindex=&#34;0&#34; style=&#34;background-color:#f0f3f3;-moz-tab-size:2;-o-tab-size:2;tab-size:2;&#34;&gt;&lt;code class=&#34;language-javascript&#34; data-lang=&#34;javascript&#34;&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;&lt;span style=&#34;color:#069;font-weight:bold&#34;&gt;const&lt;/span&gt; themes &lt;span style=&#34;color:#555&#34;&gt;=&lt;/span&gt; {
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;  light&lt;span style=&#34;color:#555&#34;&gt;:&lt;/span&gt; {
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;    name&lt;span style=&#34;color:#555&#34;&gt;:&lt;/span&gt; &lt;span style=&#34;color:#c30&#34;&gt;&#39;☀️ Light Theme&#39;&lt;/span&gt;,
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;    background&lt;span style=&#34;color:#555&#34;&gt;:&lt;/span&gt; &lt;span style=&#34;color:#c30&#34;&gt;&#39;#FFFFFF&#39;&lt;/span&gt;,
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;    text&lt;span style=&#34;color:#555&#34;&gt;:&lt;/span&gt; &lt;span style=&#34;color:#c30&#34;&gt;&#39;#000000&#39;&lt;/span&gt;,
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;    headers&lt;span style=&#34;color:#555&#34;&gt;:&lt;/span&gt; &lt;span style=&#34;color:#c30&#34;&gt;&#39;#000000&#39;&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;    links&lt;span style=&#34;color:#555&#34;&gt;:&lt;/span&gt; &lt;span style=&#34;color:#c30&#34;&gt;&#39;#01BAEF&#39;&lt;/span&gt;,
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;  },
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;  dark&lt;span style=&#34;color:#555&#34;&gt;:&lt;/span&gt; {
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;    name&lt;span style=&#34;color:#555&#34;&gt;:&lt;/span&gt; &lt;span style=&#34;color:#c30&#34;&gt;&#39;🌒 Dark Theme&#39;&lt;/span&gt;,
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;    background&lt;span style=&#34;color:#555&#34;&gt;:&lt;/span&gt; &lt;span style=&#34;color:#c30&#34;&gt;&#39;#0F0E17&#39;&lt;/span&gt;,
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;    text&lt;span style=&#34;color:#555&#34;&gt;:&lt;/span&gt; &lt;span style=&#34;color:#c30&#34;&gt;&#39;#A7A9BE&#39;&lt;/span&gt;,
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;    headers&lt;span style=&#34;color:#555&#34;&gt;:&lt;/span&gt; &lt;span style=&#34;color:#c30&#34;&gt;&#39;#FFFFFE&#39;&lt;/span&gt;,
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;    links&lt;span style=&#34;color:#555&#34;&gt;:&lt;/span&gt; &lt;span style=&#34;color:#c30&#34;&gt;&#39;#FF8906&#39;&lt;/span&gt;,
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;  },
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;};
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;&lt;span style=&#34;color:#069;font-weight:bold&#34;&gt;export&lt;/span&gt; &lt;span style=&#34;color:#069;font-weight:bold&#34;&gt;default&lt;/span&gt; themes;
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;You can add as many attributes to your themes as you like in order to increase their flexibility. I use &lt;a href=&#34;https://www.happyhues.co&#34; target=&#34;_blank&#34; rel=&#34;noopener noreferrer&#34; &gt;Happy Hues&lt;/a&gt; for inspiration. You can also create as many different themes as you like!&lt;/p&gt;
&lt;p&gt;Now we need a way to manage the themes and to maintain the currently-selected theme. This is where the Zustand library will come in useful for managing the app’s global theme state.&lt;/p&gt;
&lt;p&gt;For this, create a &lt;code&gt;store.js&lt;/code&gt; file (e.g. again, this could be in your site’s top-level &lt;code&gt;src/&lt;/code&gt; directory) with these contents:&lt;/p&gt;
&lt;div class=&#34;highlight&#34;&gt;&lt;pre tabindex=&#34;0&#34; style=&#34;background-color:#f0f3f3;-moz-tab-size:2;-o-tab-size:2;tab-size:2;&#34;&gt;&lt;code class=&#34;language-javascript&#34; data-lang=&#34;javascript&#34;&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;&lt;span style=&#34;color:#069;font-weight:bold&#34;&gt;import&lt;/span&gt; create from &lt;span style=&#34;color:#c30&#34;&gt;&#39;zustand&#39;&lt;/span&gt;;
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;&lt;span style=&#34;color:#069;font-weight:bold&#34;&gt;export&lt;/span&gt; &lt;span style=&#34;color:#069;font-weight:bold&#34;&gt;default&lt;/span&gt; create(set =&gt; ({
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;  theme&lt;span style=&#34;color:#555&#34;&gt;:&lt;/span&gt; &lt;span style=&#34;color:#c30&#34;&gt;&#39;light&#39;&lt;/span&gt;,
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;  setTheme&lt;span style=&#34;color:#555&#34;&gt;:&lt;/span&gt; theme =&gt; set({ theme }),
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;}));
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;This tells Zustand to create a new store with two attributes: a reference to the current theme key (defaulting to &lt;code&gt;light&lt;/code&gt;) and a function for changing the theme.&lt;/p&gt;
&lt;p&gt;Next, use the &lt;code&gt;styled-components&lt;/code&gt; library to create some dynamic components for use within the app that change their style based on the currently selected theme.&lt;/p&gt;
&lt;p&gt;The package’s &lt;code&gt;createGlobalStyle&lt;/code&gt; function is great for injecting styles that affect the entire app. Import the function at the top of your primary layout file:&lt;/p&gt;
&lt;div class=&#34;highlight&#34;&gt;&lt;pre tabindex=&#34;0&#34; style=&#34;background-color:#f0f3f3;-moz-tab-size:2;-o-tab-size:2;tab-size:2;&#34;&gt;&lt;code class=&#34;language-javascript&#34; data-lang=&#34;javascript&#34;&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;&lt;span style=&#34;color:#069;font-weight:bold&#34;&gt;import&lt;/span&gt; { createGlobalStyle } from &lt;span style=&#34;color:#c30&#34;&gt;&#39;styled-components&#39;&lt;/span&gt;;
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;And then use it to create a global style component for your website based on the currently-selected theme (in the same top-level layout file):&lt;/p&gt;
&lt;div class=&#34;highlight&#34;&gt;&lt;pre tabindex=&#34;0&#34; style=&#34;background-color:#f0f3f3;-moz-tab-size:2;-o-tab-size:2;tab-size:2;&#34;&gt;&lt;code class=&#34;language-javascript&#34; data-lang=&#34;javascript&#34;&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;&lt;span style=&#34;color:#069;font-weight:bold&#34;&gt;const&lt;/span&gt; GlobalStyle &lt;span style=&#34;color:#555&#34;&gt;=&lt;/span&gt; createGlobalStyle&lt;span style=&#34;color:#c30&#34;&gt;`
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;&lt;span style=&#34;color:#c30&#34;&gt;  body{
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;&lt;span style=&#34;color:#c30&#34;&gt;    background-color: &lt;/span&gt;&lt;span style=&#34;color:#a00&#34;&gt;${&lt;/span&gt;({ theme &lt;span style=&#34;color:#a00&#34;&gt;}&lt;/span&gt;&lt;span style=&#34;color:#c30&#34;&gt;) =&gt; theme.background};
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;&lt;span style=&#34;color:#c30&#34;&gt;    color: &lt;/span&gt;&lt;span style=&#34;color:#a00&#34;&gt;${&lt;/span&gt;({ theme &lt;span style=&#34;color:#a00&#34;&gt;}&lt;/span&gt;&lt;span style=&#34;color:#c30&#34;&gt;) =&gt; theme.text};
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;&lt;span style=&#34;color:#c30&#34;&gt;    transition: background-color 0.25s, color 0.25s;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;&lt;span style=&#34;color:#c30&#34;&gt;  }
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;&lt;span style=&#34;color:#c30&#34;&gt;  h1,h2,h3,h4 {
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;&lt;span style=&#34;color:#c30&#34;&gt;    color: &lt;/span&gt;&lt;span style=&#34;color:#a00&#34;&gt;${&lt;/span&gt;({ theme &lt;span style=&#34;color:#a00&#34;&gt;}&lt;/span&gt;&lt;span style=&#34;color:#c30&#34;&gt;) =&gt; theme.headers};
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;&lt;span style=&#34;color:#c30&#34;&gt;  }
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;&lt;span style=&#34;color:#c30&#34;&gt;  a {
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;&lt;span style=&#34;color:#c30&#34;&gt;    color: &lt;/span&gt;&lt;span style=&#34;color:#a00&#34;&gt;${&lt;/span&gt;({ theme &lt;span style=&#34;color:#a00&#34;&gt;}&lt;/span&gt;&lt;span style=&#34;color:#c30&#34;&gt;) =&gt; theme.links};
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;&lt;span style=&#34;color:#c30&#34;&gt;  }
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;&lt;span style=&#34;color:#c30&#34;&gt;`&lt;/span&gt;;
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;We also added a transition to fade nicely between styles when the user later switches theme.&lt;/p&gt;
&lt;p&gt;Finally, add some code (again, to the same layout file) to import the current theme from the store and pass this to the global style component we created.&lt;/p&gt;
&lt;div class=&#34;highlight&#34;&gt;&lt;pre tabindex=&#34;0&#34; style=&#34;background-color:#f0f3f3;-moz-tab-size:2;-o-tab-size:2;tab-size:2;&#34;&gt;&lt;code class=&#34;language-javascript&#34; data-lang=&#34;javascript&#34;&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;&lt;span style=&#34;color:#09f;font-style:italic&#34;&gt;// First import the store you created earlier near the top of the file:
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;&lt;span style=&#34;color:#09f;font-style:italic&#34;&gt;&lt;/span&gt;&lt;span style=&#34;color:#069;font-weight:bold&#34;&gt;import&lt;/span&gt; useStore from &lt;span style=&#34;color:#c30&#34;&gt;&#39;../../store&#39;&lt;/span&gt;;
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;&lt;span style=&#34;color:#09f;font-style:italic&#34;&gt;// Import the themes from the theme file you created earlier:
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;&lt;span style=&#34;color:#09f;font-style:italic&#34;&gt;&lt;/span&gt;&lt;span style=&#34;color:#069;font-weight:bold&#34;&gt;import&lt;/span&gt; themes from &lt;span style=&#34;color:#c30&#34;&gt;&#39;../../themes&#39;&lt;/span&gt;;
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;...
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;&lt;span style=&#34;color:#09f;font-style:italic&#34;&gt;// In your main layout component load the theme from the store:
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;&lt;span style=&#34;color:#09f;font-style:italic&#34;&gt;&lt;/span&gt;&lt;span style=&#34;color:#069;font-weight:bold&#34;&gt;const&lt;/span&gt; LayoutComponent &lt;span style=&#34;color:#555&#34;&gt;=&lt;/span&gt; ({ children }) =&gt; {
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;  &lt;span style=&#34;color:#069;font-weight:bold&#34;&gt;const&lt;/span&gt; theme &lt;span style=&#34;color:#555&#34;&gt;=&lt;/span&gt; useStore(s =&gt; s.theme);
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;  ...
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;  
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;  &lt;span style=&#34;color:#09f;font-style:italic&#34;&gt;// Then render the global styles and the layout&#39;s children:
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;&lt;span style=&#34;color:#09f;font-style:italic&#34;&gt;&lt;/span&gt;  &lt;span style=&#34;color:#069;font-weight:bold&#34;&gt;return&lt;/span&gt; (
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;    &lt;span style=&#34;color:#555&#34;&gt;&lt;&lt;/span&gt;div&lt;span style=&#34;color:#555&#34;&gt;&gt;&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;      &lt;span style=&#34;color:#555&#34;&gt;&lt;&lt;/span&gt;GlobalStyle theme&lt;span style=&#34;color:#555&#34;&gt;=&lt;/span&gt;{themes[theme]} &lt;span style=&#34;color:#555&#34;&gt;/&gt;&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;      &lt;span style=&#34;color:#555&#34;&gt;&lt;&lt;/span&gt;div&lt;span style=&#34;color:#555&#34;&gt;&gt;&lt;/span&gt;{children}&lt;span style=&#34;color:#555&#34;&gt;&lt;&lt;/span&gt;&lt;span style=&#34;color:#a00;background-color:#faa&#34;&gt;/div&gt;&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;    &lt;span style=&#34;color:#555&#34;&gt;&lt;&lt;/span&gt;&lt;span style=&#34;color:#a00;background-color:#faa&#34;&gt;/div&gt;&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;  );
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;}
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;Your website is now using the theme to set the colours you defined in your theme object. If you change the default theme (in the &lt;code&gt;store.js&lt;/code&gt; file) to &lt;code&gt;dark&lt;/code&gt; and refresh the page, you should see the styles for the dark theme rendered.&lt;/p&gt;
&lt;h2 id=&#34;changing-the-theme&#34;&gt;Changing the theme&lt;/h2&gt;
&lt;p&gt;We now need to provide a way for the user to change the website’s theme. Typically this might be done in a dedicated component elsewhere in your app, which is where using the Zustand store comes in handy.&lt;/p&gt;
&lt;p&gt;Currently, on this website, I have a change theme select box in the site’s &lt;code&gt;Header&lt;/code&gt; component (as shown in the image below):&lt;/p&gt;
&lt;p&gt;&lt;img src=&#34;https://wilw.dev/media/blog/theming1.png&#34; alt=&#34;The change theme select box on my website&#34;&gt;&lt;/p&gt;
&lt;p&gt;This can be achieved through something like the following:&lt;/p&gt;
&lt;div class=&#34;highlight&#34;&gt;&lt;pre tabindex=&#34;0&#34; style=&#34;background-color:#f0f3f3;-moz-tab-size:2;-o-tab-size:2;tab-size:2;&#34;&gt;&lt;code class=&#34;language-javascript&#34; data-lang=&#34;javascript&#34;&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;&lt;span style=&#34;color:#09f;font-style:italic&#34;&gt;// Import the styled-components library:
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;&lt;span style=&#34;color:#09f;font-style:italic&#34;&gt;&lt;/span&gt;&lt;span style=&#34;color:#069;font-weight:bold&#34;&gt;import&lt;/span&gt; styled from &lt;span style=&#34;color:#c30&#34;&gt;&#39;styled-components&#39;&lt;/span&gt;;
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;&lt;span style=&#34;color:#09f;font-style:italic&#34;&gt;// Import the store we created earlier:
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;&lt;span style=&#34;color:#09f;font-style:italic&#34;&gt;&lt;/span&gt;&lt;span style=&#34;color:#069;font-weight:bold&#34;&gt;import&lt;/span&gt; useStore from &lt;span style=&#34;color:#c30&#34;&gt;&#39;../store&#39;&lt;/span&gt;;
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;&lt;span style=&#34;color:#09f;font-style:italic&#34;&gt;// Import the theme list we created earlier:
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;&lt;span style=&#34;color:#09f;font-style:italic&#34;&gt;&lt;/span&gt;&lt;span style=&#34;color:#069;font-weight:bold&#34;&gt;import&lt;/span&gt; themes from &lt;span style=&#34;color:#c30&#34;&gt;&#39;../themes&#39;&lt;/span&gt;;
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;&lt;span style=&#34;color:#09f;font-style:italic&#34;&gt;// Use styled-components to create a nice select box:
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;&lt;span style=&#34;color:#09f;font-style:italic&#34;&gt;&lt;/span&gt;&lt;span style=&#34;color:#069;font-weight:bold&#34;&gt;const&lt;/span&gt; StyledThemeSelector &lt;span style=&#34;color:#555&#34;&gt;=&lt;/span&gt; styled.select&lt;span style=&#34;color:#c30&#34;&gt;`
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;&lt;span style=&#34;color:#c30&#34;&gt;  padding: 4px;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;&lt;span style=&#34;color:#c30&#34;&gt;  background: white;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;&lt;span style=&#34;color:#c30&#34;&gt;  border: 1px solid gray;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;&lt;span style=&#34;color:#c30&#34;&gt;  border-radius: 5px;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;&lt;span style=&#34;color:#c30&#34;&gt;  cursor: pointer;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;&lt;span style=&#34;color:#c30&#34;&gt;`&lt;/span&gt;;
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;&lt;span style=&#34;color:#09f;font-style:italic&#34;&gt;// In the component load the store:
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;&lt;span style=&#34;color:#09f;font-style:italic&#34;&gt;&lt;/span&gt;&lt;span style=&#34;color:#069;font-weight:bold&#34;&gt;const&lt;/span&gt; Header &lt;span style=&#34;color:#555&#34;&gt;=&lt;/span&gt; () =&gt; {
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;  &lt;span style=&#34;color:#069;font-weight:bold&#34;&gt;const&lt;/span&gt; store &lt;span style=&#34;color:#555&#34;&gt;=&lt;/span&gt; useStore();
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;  &lt;span style=&#34;color:#09f;font-style:italic&#34;&gt;// When rendering the component, include your nicely styled theme selector:
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;&lt;span style=&#34;color:#09f;font-style:italic&#34;&gt;&lt;/span&gt;  &lt;span style=&#34;color:#069;font-weight:bold&#34;&gt;return&lt;/span&gt; ( &lt;span style=&#34;color:#555&#34;&gt;&lt;&gt;&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;    ...
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;    &lt;span style=&#34;color:#555&#34;&gt;&lt;&lt;/span&gt;StyledThemeSelector value&lt;span style=&#34;color:#555&#34;&gt;=&lt;/span&gt;{store.theme} onChange&lt;span style=&#34;color:#555&#34;&gt;=&lt;/span&gt;{e =&gt; store.setTheme(e.target.value)}&lt;span style=&#34;color:#555&#34;&gt;&gt;&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;      {&lt;span style=&#34;color:#366&#34;&gt;Object&lt;/span&gt;.keys(themes).map(themeName =&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;        &lt;span style=&#34;color:#555&#34;&gt;&lt;&lt;/span&gt;option key&lt;span style=&#34;color:#555&#34;&gt;=&lt;/span&gt;{themeName} value&lt;span style=&#34;color:#555&#34;&gt;=&lt;/span&gt;{themeName}&lt;span style=&#34;color:#555&#34;&gt;&gt;&lt;/span&gt;{themes[themeName].name}&lt;span style=&#34;color:#555&#34;&gt;&lt;&lt;/span&gt;&lt;span style=&#34;color:#a00;background-color:#faa&#34;&gt;/option&gt;&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;      )}
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;    &lt;span style=&#34;color:#555&#34;&gt;&lt;&lt;/span&gt;&lt;span style=&#34;color:#a00;background-color:#faa&#34;&gt;/StyledThemeSelector&gt;&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;    ...
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;  &lt;span style=&#34;color:#555&#34;&gt;&lt;&lt;/span&gt;&lt;span style=&#34;color:#a00;background-color:#faa&#34;&gt;/&gt; );&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;}
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;Now, when you switch theme using the select box, the entire app should change to reflect the new styles. Also, because you added the &lt;code&gt;transition&lt;/code&gt; to the styles earlier, the new colours should fade in nicely rather than suddenly changing.&lt;/p&gt;
&lt;p&gt;Of course, the theme-selctor can be any type of component - a button, a list of radio buttons, or anything else. If you have lots of themes you could even create a more visual theme gallery.&lt;/p&gt;
&lt;h2 id=&#34;remembering-the-selected-theme&#34;&gt;Remembering the selected theme&lt;/h2&gt;
&lt;p&gt;You can configure your website to remember the user’s theme choice by leveraging &lt;code&gt;localStorage&lt;/code&gt;.&lt;/p&gt;
&lt;p&gt;To do so, first update your theme-selection component (as above) to implement a function which updates the site’s local storage each time the user changes the theme:&lt;/p&gt;
&lt;div class=&#34;highlight&#34;&gt;&lt;pre tabindex=&#34;0&#34; style=&#34;background-color:#f0f3f3;-moz-tab-size:2;-o-tab-size:2;tab-size:2;&#34;&gt;&lt;code class=&#34;language-javascript&#34; data-lang=&#34;javascript&#34;&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;...
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;&lt;span style=&#34;color:#069;font-weight:bold&#34;&gt;const&lt;/span&gt; Header &lt;span style=&#34;color:#555&#34;&gt;=&lt;/span&gt; () =&gt; {
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;  &lt;span style=&#34;color:#069;font-weight:bold&#34;&gt;const&lt;/span&gt; store &lt;span style=&#34;color:#555&#34;&gt;=&lt;/span&gt; useStore();
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;  &lt;span style=&#34;color:#09f;font-style:italic&#34;&gt;// Add this function:
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;&lt;span style=&#34;color:#09f;font-style:italic&#34;&gt;&lt;/span&gt;  &lt;span style=&#34;color:#069;font-weight:bold&#34;&gt;const&lt;/span&gt; switchTheme &lt;span style=&#34;color:#555&#34;&gt;=&lt;/span&gt; themeName =&gt; {
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;    store.setTheme(themeName);
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;    localStorage.setItem(&lt;span style=&#34;color:#c30&#34;&gt;&#39;theme&#39;&lt;/span&gt;, themeName);
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;  };
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;  &lt;span style=&#34;color:#09f;font-style:italic&#34;&gt;// And update the onChange for your theme selector:
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;&lt;span style=&#34;color:#09f;font-style:italic&#34;&gt;&lt;/span&gt;  &lt;span style=&#34;color:#069;font-weight:bold&#34;&gt;return&lt;/span&gt; ( &lt;span style=&#34;color:#555&#34;&gt;&lt;&gt;&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;    ...
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;    &lt;span style=&#34;color:#555&#34;&gt;&lt;&lt;/span&gt;StyledThemeSelector value&lt;span style=&#34;color:#555&#34;&gt;=&lt;/span&gt;{store.theme} onChange&lt;span style=&#34;color:#555&#34;&gt;=&lt;/span&gt;{e =&gt; switchTheme(e.target.value)}&lt;span style=&#34;color:#555&#34;&gt;&gt;&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;      ...
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;    &lt;span style=&#34;color:#555&#34;&gt;&lt;&lt;/span&gt;&lt;span style=&#34;color:#a00;background-color:#faa&#34;&gt;/StyledThemeSelector&gt;&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;    ...
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;  &lt;span style=&#34;color:#555&#34;&gt;&lt;&lt;/span&gt;&lt;span style=&#34;color:#a00;background-color:#faa&#34;&gt;/&gt; );&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;}
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;Then, back in your top-level layout file, add a &lt;code&gt;useEffect&lt;/code&gt; hook to update the theme on app-load:&lt;/p&gt;
&lt;div class=&#34;highlight&#34;&gt;&lt;pre tabindex=&#34;0&#34; style=&#34;background-color:#f0f3f3;-moz-tab-size:2;-o-tab-size:2;tab-size:2;&#34;&gt;&lt;code class=&#34;language-javascript&#34; data-lang=&#34;javascript&#34;&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;&lt;span style=&#34;color:#09f;font-style:italic&#34;&gt;// Add the useEffect import:
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;&lt;span style=&#34;color:#09f;font-style:italic&#34;&gt;&lt;/span&gt;&lt;span style=&#34;color:#069;font-weight:bold&#34;&gt;import&lt;/span&gt; React, { useEffect } from &lt;span style=&#34;color:#c30&#34;&gt;&#39;react&#39;&lt;/span&gt;;
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;...
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;&lt;span style=&#34;color:#069;font-weight:bold&#34;&gt;const&lt;/span&gt; LayoutComponent &lt;span style=&#34;color:#555&#34;&gt;=&lt;/span&gt; ({ children }) =&gt; {
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;  &lt;span style=&#34;color:#069;font-weight:bold&#34;&gt;const&lt;/span&gt; setTheme &lt;span style=&#34;color:#555&#34;&gt;=&lt;/span&gt; useStore(s =&gt; s.setTheme);
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;  ...
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;  useEffect(() =&gt; {
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;    &lt;span style=&#34;color:#09f;font-style:italic&#34;&gt;// Load and set the stored theme
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;&lt;span style=&#34;color:#09f;font-style:italic&#34;&gt;&lt;/span&gt;    &lt;span style=&#34;color:#069;font-weight:bold&#34;&gt;const&lt;/span&gt; rememberedTheme &lt;span style=&#34;color:#555&#34;&gt;=&lt;/span&gt; localStorage.getItem(&lt;span style=&#34;color:#c30&#34;&gt;&#39;theme&#39;&lt;/span&gt;);
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;    &lt;span style=&#34;color:#069;font-weight:bold&#34;&gt;if&lt;/span&gt; (rememberedTheme &lt;span style=&#34;color:#555&#34;&gt;&amp;&amp;&lt;/span&gt; themes[rememberedTheme]) {
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;      setTheme(rememberedTheme);
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;    } 
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;  }, [setTheme]);
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;   ...
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;}
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;Now the website will load the user’s preferred theme each time they come back to your site.&lt;/p&gt;
&lt;h2 id=&#34;automatically-matching-the-operating-system-theme&#34;&gt;Automatically matching the operating system theme&lt;/h2&gt;
&lt;p&gt;Many operating systems allow the user to choose between a light and dark theme, or even to dynamically change the theme based on the time of day.&lt;/p&gt;
&lt;p&gt;It can be seen as good practice for websites to display a theme which more closely matches the background OS style to provide a more consistent user experience.&lt;/p&gt;
&lt;p&gt;Luckily, this is very easy to do. Just update the &lt;code&gt;useEffect&lt;/code&gt; block described above to check for the OS ‘dark mode’ as the app loads.&lt;/p&gt;
&lt;div class=&#34;highlight&#34;&gt;&lt;pre tabindex=&#34;0&#34; style=&#34;background-color:#f0f3f3;-moz-tab-size:2;-o-tab-size:2;tab-size:2;&#34;&gt;&lt;code class=&#34;language-javascript&#34; data-lang=&#34;javascript&#34;&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;...
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;  useEffect(() =&gt; {
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;    &lt;span style=&#34;color:#069;font-weight:bold&#34;&gt;const&lt;/span&gt; rememberedTheme &lt;span style=&#34;color:#555&#34;&gt;=&lt;/span&gt; localStorage.getItem(&lt;span style=&#34;color:#c30&#34;&gt;&#39;theme&#39;&lt;/span&gt;);
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;    &lt;span style=&#34;color:#069;font-weight:bold&#34;&gt;if&lt;/span&gt; (rememberedTheme &lt;span style=&#34;color:#555&#34;&gt;&amp;&amp;&lt;/span&gt; themes[rememberedTheme]) {
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;      setTheme(rememberedTheme);
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;    } &lt;span style=&#34;color:#069;font-weight:bold&#34;&gt;else&lt;/span&gt; {
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;      &lt;span style=&#34;color:#069;font-weight:bold&#34;&gt;const&lt;/span&gt; isDarkMode &lt;span style=&#34;color:#555&#34;&gt;=&lt;/span&gt; &lt;span style=&#34;color:#366&#34;&gt;window&lt;/span&gt;.matchMedia &lt;span style=&#34;color:#555&#34;&gt;&amp;&amp;&lt;/span&gt; &lt;span style=&#34;color:#366&#34;&gt;window&lt;/span&gt;.matchMedia(&lt;span style=&#34;color:#c30&#34;&gt;&#39;(prefers-color-scheme: dark)&#39;&lt;/span&gt;).matches;
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;      &lt;span style=&#34;color:#069;font-weight:bold&#34;&gt;if&lt;/span&gt; (isDarkMode) setTheme(&lt;span style=&#34;color:#c30&#34;&gt;&#39;dark&#39;&lt;/span&gt;);
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;    }
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;  }, [setTheme]);
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;...
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;The nice thing about this approach is that if the user has already chosen another theme preference, this will be used instead of the default.&lt;/p&gt;
&lt;h2 id=&#34;complete-code&#34;&gt;Complete code&lt;/h2&gt;
&lt;p&gt;I hope this post helps you to create your own theme libraries and nice style-switchers. As mentioned earlier, you could create as many themes as you like and make them as complex as necessary.&lt;/p&gt;
&lt;p&gt;You can also import the store into any component if you want more fine-grained control (e.g. themed message boxes), and even include fonts and other types of styles.&lt;/p&gt;
&lt;p&gt;For the complete code discussed in this post, see below.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;/src/themes.js&lt;/strong&gt;&lt;/p&gt;
&lt;div class=&#34;highlight&#34;&gt;&lt;pre tabindex=&#34;0&#34; style=&#34;background-color:#f0f3f3;-moz-tab-size:2;-o-tab-size:2;tab-size:2;&#34;&gt;&lt;code class=&#34;language-javascript&#34; data-lang=&#34;javascript&#34;&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;&lt;span style=&#34;color:#069;font-weight:bold&#34;&gt;const&lt;/span&gt; themes &lt;span style=&#34;color:#555&#34;&gt;=&lt;/span&gt; {
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;  light&lt;span style=&#34;color:#555&#34;&gt;:&lt;/span&gt; {
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;    name&lt;span style=&#34;color:#555&#34;&gt;:&lt;/span&gt; &lt;span style=&#34;color:#c30&#34;&gt;&#39;☀️ Light Theme&#39;&lt;/span&gt;,
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;    background&lt;span style=&#34;color:#555&#34;&gt;:&lt;/span&gt; &lt;span style=&#34;color:#c30&#34;&gt;&#39;#FFFFFF&#39;&lt;/span&gt;,
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;    text&lt;span style=&#34;color:#555&#34;&gt;:&lt;/span&gt; &lt;span style=&#34;color:#c30&#34;&gt;&#39;#000000&#39;&lt;/span&gt;,
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;    headers&lt;span style=&#34;color:#555&#34;&gt;:&lt;/span&gt; &lt;span style=&#34;color:#c30&#34;&gt;&#39;#000000&#39;&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;    links&lt;span style=&#34;color:#555&#34;&gt;:&lt;/span&gt; &lt;span style=&#34;color:#c30&#34;&gt;&#39;#01BAEF&#39;&lt;/span&gt;,
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;  },
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;  dark&lt;span style=&#34;color:#555&#34;&gt;:&lt;/span&gt; {
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;    name&lt;span style=&#34;color:#555&#34;&gt;:&lt;/span&gt; &lt;span style=&#34;color:#c30&#34;&gt;&#39;🌒 Dark Theme&#39;&lt;/span&gt;,
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;    background&lt;span style=&#34;color:#555&#34;&gt;:&lt;/span&gt; &lt;span style=&#34;color:#c30&#34;&gt;&#39;#0F0E17&#39;&lt;/span&gt;,
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;    text&lt;span style=&#34;color:#555&#34;&gt;:&lt;/span&gt; &lt;span style=&#34;color:#c30&#34;&gt;&#39;#A7A9BE&#39;&lt;/span&gt;,
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;    headers&lt;span style=&#34;color:#555&#34;&gt;:&lt;/span&gt; &lt;span style=&#34;color:#c30&#34;&gt;&#39;#FFFFFE&#39;&lt;/span&gt;,
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;    links&lt;span style=&#34;color:#555&#34;&gt;:&lt;/span&gt; &lt;span style=&#34;color:#c30&#34;&gt;&#39;#FF8906&#39;&lt;/span&gt;,
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;  },
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;};
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;&lt;span style=&#34;color:#069;font-weight:bold&#34;&gt;export&lt;/span&gt; &lt;span style=&#34;color:#069;font-weight:bold&#34;&gt;default&lt;/span&gt; themes;
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;&lt;strong&gt;/src/store.js&lt;/strong&gt;&lt;/p&gt;
&lt;div class=&#34;highlight&#34;&gt;&lt;pre tabindex=&#34;0&#34; style=&#34;background-color:#f0f3f3;-moz-tab-size:2;-o-tab-size:2;tab-size:2;&#34;&gt;&lt;code class=&#34;language-javascript&#34; data-lang=&#34;javascript&#34;&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;&lt;span style=&#34;color:#069;font-weight:bold&#34;&gt;import&lt;/span&gt; create from &lt;span style=&#34;color:#c30&#34;&gt;&#39;zustand&#39;&lt;/span&gt;;
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;&lt;span style=&#34;color:#069;font-weight:bold&#34;&gt;export&lt;/span&gt; &lt;span style=&#34;color:#069;font-weight:bold&#34;&gt;default&lt;/span&gt; create(set =&gt; ({
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;  theme&lt;span style=&#34;color:#555&#34;&gt;:&lt;/span&gt; &lt;span style=&#34;color:#c30&#34;&gt;&#39;light&#39;&lt;/span&gt;,
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;  setTheme&lt;span style=&#34;color:#555&#34;&gt;:&lt;/span&gt; theme =&gt; set({ theme }),
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;}));
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;&lt;strong&gt;Top-level layout/app file&lt;/strong&gt;&lt;/p&gt;
&lt;div class=&#34;highlight&#34;&gt;&lt;pre tabindex=&#34;0&#34; style=&#34;background-color:#f0f3f3;-moz-tab-size:2;-o-tab-size:2;tab-size:2;&#34;&gt;&lt;code class=&#34;language-javascript&#34; data-lang=&#34;javascript&#34;&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;&lt;span style=&#34;color:#069;font-weight:bold&#34;&gt;import&lt;/span&gt; React, { useEffect } from &lt;span style=&#34;color:#c30&#34;&gt;&#39;react&#39;&lt;/span&gt;;
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;&lt;span style=&#34;color:#069;font-weight:bold&#34;&gt;import&lt;/span&gt; { createGlobalStyle } from &lt;span style=&#34;color:#c30&#34;&gt;&#39;styled-components&#39;&lt;/span&gt;;
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;&lt;span style=&#34;color:#069;font-weight:bold&#34;&gt;import&lt;/span&gt; useStore from &lt;span style=&#34;color:#c30&#34;&gt;&#39;../../store&#39;&lt;/span&gt;;
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;&lt;span style=&#34;color:#069;font-weight:bold&#34;&gt;import&lt;/span&gt; themes from &lt;span style=&#34;color:#c30&#34;&gt;&#39;../../themes&#39;&lt;/span&gt;;
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;&lt;span style=&#34;color:#069;font-weight:bold&#34;&gt;const&lt;/span&gt; GlobalStyle &lt;span style=&#34;color:#555&#34;&gt;=&lt;/span&gt; createGlobalStyle&lt;span style=&#34;color:#c30&#34;&gt;`
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;&lt;span style=&#34;color:#c30&#34;&gt;  body{
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;&lt;span style=&#34;color:#c30&#34;&gt;    background-color: &lt;/span&gt;&lt;span style=&#34;color:#a00&#34;&gt;${&lt;/span&gt;({ theme &lt;span style=&#34;color:#a00&#34;&gt;}&lt;/span&gt;&lt;span style=&#34;color:#c30&#34;&gt;) =&gt; theme.background};
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;&lt;span style=&#34;color:#c30&#34;&gt;    color: &lt;/span&gt;&lt;span style=&#34;color:#a00&#34;&gt;${&lt;/span&gt;({ theme &lt;span style=&#34;color:#a00&#34;&gt;}&lt;/span&gt;&lt;span style=&#34;color:#c30&#34;&gt;) =&gt; theme.text};
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;&lt;span style=&#34;color:#c30&#34;&gt;    transition: background-color 0.25s, color 0.25s;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;&lt;span style=&#34;color:#c30&#34;&gt;  }
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;&lt;span style=&#34;color:#c30&#34;&gt;  h1,h2,h3,h4 {
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;&lt;span style=&#34;color:#c30&#34;&gt;    color: &lt;/span&gt;&lt;span style=&#34;color:#a00&#34;&gt;${&lt;/span&gt;({ theme &lt;span style=&#34;color:#a00&#34;&gt;}&lt;/span&gt;&lt;span style=&#34;color:#c30&#34;&gt;) =&gt; theme.headers};
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;&lt;span style=&#34;color:#c30&#34;&gt;  }
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;&lt;span style=&#34;color:#c30&#34;&gt;  a {
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;&lt;span style=&#34;color:#c30&#34;&gt;    color: &lt;/span&gt;&lt;span style=&#34;color:#a00&#34;&gt;${&lt;/span&gt;({ theme &lt;span style=&#34;color:#a00&#34;&gt;}&lt;/span&gt;&lt;span style=&#34;color:#c30&#34;&gt;) =&gt; theme.links};
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;&lt;span style=&#34;color:#c30&#34;&gt;  }
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;&lt;span style=&#34;color:#c30&#34;&gt;`&lt;/span&gt;;
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;&lt;span style=&#34;color:#069;font-weight:bold&#34;&gt;const&lt;/span&gt; LayoutComponent &lt;span style=&#34;color:#555&#34;&gt;=&lt;/span&gt; ({ children }) =&gt; {
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;  &lt;span style=&#34;color:#069;font-weight:bold&#34;&gt;const&lt;/span&gt; theme &lt;span style=&#34;color:#555&#34;&gt;=&lt;/span&gt; useStore(s =&gt; s.theme);
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;  &lt;span style=&#34;color:#069;font-weight:bold&#34;&gt;const&lt;/span&gt; setTheme &lt;span style=&#34;color:#555&#34;&gt;=&lt;/span&gt; useStore(s =&gt; s.setTheme);
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;  ...
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;  useEffect(() =&gt; {
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;    &lt;span style=&#34;color:#069;font-weight:bold&#34;&gt;const&lt;/span&gt; rememberedTheme &lt;span style=&#34;color:#555&#34;&gt;=&lt;/span&gt; localStorage.getItem(&lt;span style=&#34;color:#c30&#34;&gt;&#39;theme&#39;&lt;/span&gt;);
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;    &lt;span style=&#34;color:#069;font-weight:bold&#34;&gt;if&lt;/span&gt; (rememberedTheme &lt;span style=&#34;color:#555&#34;&gt;&amp;&amp;&lt;/span&gt; themes[rememberedTheme]) {
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;      setTheme(rememberedTheme);
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;    } &lt;span style=&#34;color:#069;font-weight:bold&#34;&gt;else&lt;/span&gt; {
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;      &lt;span style=&#34;color:#069;font-weight:bold&#34;&gt;const&lt;/span&gt; isDarkMode &lt;span style=&#34;color:#555&#34;&gt;=&lt;/span&gt; &lt;span style=&#34;color:#366&#34;&gt;window&lt;/span&gt;.matchMedia &lt;span style=&#34;color:#555&#34;&gt;&amp;&amp;&lt;/span&gt; &lt;span style=&#34;color:#366&#34;&gt;window&lt;/span&gt;.matchMedia(&lt;span style=&#34;color:#c30&#34;&gt;&#39;(prefers-color-scheme: dark)&#39;&lt;/span&gt;).matches;
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;      &lt;span style=&#34;color:#069;font-weight:bold&#34;&gt;if&lt;/span&gt; (isDarkMode) setTheme(&lt;span style=&#34;color:#c30&#34;&gt;&#39;dark&#39;&lt;/span&gt;);
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;    }
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;  }, [setTheme]);
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;  ...
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;  &lt;span style=&#34;color:#069;font-weight:bold&#34;&gt;return&lt;/span&gt; (
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;    &lt;span style=&#34;color:#555&#34;&gt;&lt;&lt;/span&gt;div&lt;span style=&#34;color:#555&#34;&gt;&gt;&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;      &lt;span style=&#34;color:#555&#34;&gt;&lt;&lt;/span&gt;GlobalStyle theme&lt;span style=&#34;color:#555&#34;&gt;=&lt;/span&gt;{themes[theme]} &lt;span style=&#34;color:#555&#34;&gt;/&gt;&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;      &lt;span style=&#34;color:#555&#34;&gt;&lt;&lt;/span&gt;div&lt;span style=&#34;color:#555&#34;&gt;&gt;&lt;/span&gt;{children}&lt;span style=&#34;color:#555&#34;&gt;&lt;&lt;/span&gt;&lt;span style=&#34;color:#a00;background-color:#faa&#34;&gt;/div&gt;&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;    &lt;span style=&#34;color:#555&#34;&gt;&lt;&lt;/span&gt;&lt;span style=&#34;color:#a00;background-color:#faa&#34;&gt;/div&gt;&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;  );
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;}
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;&lt;span style=&#34;color:#069;font-weight:bold&#34;&gt;export&lt;/span&gt; &lt;span style=&#34;color:#069;font-weight:bold&#34;&gt;default&lt;/span&gt; LayoutComponent;
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;&lt;strong&gt;Another component (or wherever you want your theme-switcher to be)&lt;/strong&gt;&lt;/p&gt;
&lt;div class=&#34;highlight&#34;&gt;&lt;pre tabindex=&#34;0&#34; style=&#34;background-color:#f0f3f3;-moz-tab-size:2;-o-tab-size:2;tab-size:2;&#34;&gt;&lt;code class=&#34;language-javascript&#34; data-lang=&#34;javascript&#34;&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;&lt;span style=&#34;color:#069;font-weight:bold&#34;&gt;import&lt;/span&gt; React from &lt;span style=&#34;color:#c30&#34;&gt;&#39;react&#39;&lt;/span&gt;;
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;&lt;span style=&#34;color:#069;font-weight:bold&#34;&gt;import&lt;/span&gt; styled from &lt;span style=&#34;color:#c30&#34;&gt;&#39;styled-components&#39;&lt;/span&gt;;
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;&lt;span style=&#34;color:#069;font-weight:bold&#34;&gt;import&lt;/span&gt; useStore from &lt;span style=&#34;color:#c30&#34;&gt;&#39;../store&#39;&lt;/span&gt;;
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;&lt;span style=&#34;color:#069;font-weight:bold&#34;&gt;import&lt;/span&gt; themes from &lt;span style=&#34;color:#c30&#34;&gt;&#39;../themes&#39;&lt;/span&gt;;
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;&lt;span style=&#34;color:#069;font-weight:bold&#34;&gt;const&lt;/span&gt; StyledThemeSelector &lt;span style=&#34;color:#555&#34;&gt;=&lt;/span&gt; styled.select&lt;span style=&#34;color:#c30&#34;&gt;`
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;&lt;span style=&#34;color:#c30&#34;&gt;  padding: 4px;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;&lt;span style=&#34;color:#c30&#34;&gt;  background: white;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;&lt;span style=&#34;color:#c30&#34;&gt;  border: 1px solid gray;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;&lt;span style=&#34;color:#c30&#34;&gt;  border-radius: 5px;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;&lt;span style=&#34;color:#c30&#34;&gt;  cursor: pointer;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;&lt;span style=&#34;color:#c30&#34;&gt;`&lt;/span&gt;;
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;&lt;span style=&#34;color:#069;font-weight:bold&#34;&gt;const&lt;/span&gt; Header &lt;span style=&#34;color:#555&#34;&gt;=&lt;/span&gt; () =&gt; {
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;  &lt;span style=&#34;color:#069;font-weight:bold&#34;&gt;const&lt;/span&gt; store &lt;span style=&#34;color:#555&#34;&gt;=&lt;/span&gt; useStore();
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;  &lt;span style=&#34;color:#069;font-weight:bold&#34;&gt;const&lt;/span&gt; switchTheme &lt;span style=&#34;color:#555&#34;&gt;=&lt;/span&gt; themeName =&gt; {
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;    store.setTheme(themeName);
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;    localStorage.setItem(&lt;span style=&#34;color:#c30&#34;&gt;&#39;theme&#39;&lt;/span&gt;, themeName);
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;  };
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;  &lt;span style=&#34;color:#069;font-weight:bold&#34;&gt;return&lt;/span&gt; ( &lt;span style=&#34;color:#555&#34;&gt;&lt;&gt;&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;    ...
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;    &lt;span style=&#34;color:#555&#34;&gt;&lt;&lt;/span&gt;StyledThemeSelector value&lt;span style=&#34;color:#555&#34;&gt;=&lt;/span&gt;{store.theme} onChange&lt;span style=&#34;color:#555&#34;&gt;=&lt;/span&gt;{e =&gt; switchTheme(e.target.value)}&lt;span style=&#34;color:#555&#34;&gt;&gt;&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;      {&lt;span style=&#34;color:#366&#34;&gt;Object&lt;/span&gt;.keys(themes).map(themeName =&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;        &lt;span style=&#34;color:#555&#34;&gt;&lt;&lt;/span&gt;option key&lt;span style=&#34;color:#555&#34;&gt;=&lt;/span&gt;{themeName} value&lt;span style=&#34;color:#555&#34;&gt;=&lt;/span&gt;{themeName}&lt;span style=&#34;color:#555&#34;&gt;&gt;&lt;/span&gt;{themes[themeName].name}&lt;span style=&#34;color:#555&#34;&gt;&lt;&lt;/span&gt;&lt;span style=&#34;color:#a00;background-color:#faa&#34;&gt;/option&gt;&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;      )}
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;    &lt;span style=&#34;color:#555&#34;&gt;&lt;&lt;/span&gt;&lt;span style=&#34;color:#a00;background-color:#faa&#34;&gt;/StyledThemeSelector&gt;&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;    ...
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;  &lt;span style=&#34;color:#555&#34;&gt;&lt;&lt;/span&gt;&lt;span style=&#34;color:#a00;background-color:#faa&#34;&gt;/&gt; );&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;}
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;&lt;span style=&#34;color:#069;font-weight:bold&#34;&gt;export&lt;/span&gt; &lt;span style=&#34;color:#069;font-weight:bold&#34;&gt;default&lt;/span&gt; Header;
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;</content:encoded>
    </item>
    
    <item>
      <title>Starting out with BookWyrm</title>
      <link>https://wilw.dev/blog/2021/08/20/bookwyrm/</link>
      <pubDate>Fri, 20 Aug 2021 16:18:00 +0000</pubDate>
      
      <guid>https://wilw.dev/blog/2021/08/20/bookwyrm/</guid>
      
        <category>100daystooffload</category>
      
        <category>technology</category>
      
      
      <content:encoded>&lt;p&gt;For several years I’ve been a user of &lt;a href=&#34;https://www.goodreads.com&#34; target=&#34;_blank&#34; rel=&#34;noopener noreferrer&#34; &gt;Goodreads&lt;/a&gt;. It’s a very popular platform, and I primarily use it for keeping track of the books I’ve read, for receiving suggestions about new books, and for keeping up with what some of my friends are reading.&lt;/p&gt;
&lt;p&gt;It’s a good service (though sometimes a little slow) - the website and mobile app are nice to use. However, as with any closed system, it’s always a worry of mine to think about what might happen if the service were to disappear or if I were to get locked out for some reason.&lt;/p&gt;
&lt;p&gt;More recently I’ve been watching the progress and development of &lt;a href=&#34;https://github.com/mouse-reeve/bookwyrm&#34; target=&#34;_blank&#34; rel=&#34;noopener noreferrer&#34; &gt;BookWyrm&lt;/a&gt; - a decentralised and federated (through &lt;a href=&#34;https://en.wikipedia.org/wiki/ActivityPub&#34; target=&#34;_blank&#34; rel=&#34;noopener noreferrer&#34; &gt;ActivityPub&lt;/a&gt;) social reading platform. As such it can co-operate with other “fediverse” services, such as &lt;a href=&#34;https://joinmastodon.org&#34; target=&#34;_blank&#34; rel=&#34;noopener noreferrer&#34; &gt;Mastodon&lt;/a&gt;. The platform aims to allow users to record their reading activity, reviews, and follow others.&lt;/p&gt;
&lt;p&gt;The &lt;a href=&#34;https://joinbookwyrm.com&#34; target=&#34;_blank&#34; rel=&#34;noopener noreferrer&#34; &gt;Join BookWyrm&lt;/a&gt; website advertises some known &lt;a href=&#34;https://joinbookwyrm.com/instances&#34; target=&#34;_blank&#34; rel=&#34;noopener noreferrer&#34; &gt;instances&lt;/a&gt; on the web. Although I understand the underlying technology project powering these instances is still in its relatively early stages, I was excited to try one out for myself.&lt;/p&gt;
&lt;p&gt;I signed-up to the &lt;a href=&#34;https://bookwyrm.social&#34; target=&#34;_blank&#34; rel=&#34;noopener noreferrer&#34; &gt;bookwyrm.social&lt;/a&gt; instance and set up my &lt;a href=&#34;https://bookwyrm.social/user/wilw&#34; target=&#34;_blank&#34; rel=&#34;noopener noreferrer&#34; &gt;profile&lt;/a&gt;.&lt;/p&gt;
&lt;p&gt;&lt;img src=&#34;https://wilw.dev/media/blog/bookwyrm1.png&#34; alt=&#34;Importing titles from Goodreads&#34;&gt;&lt;/p&gt;
&lt;p&gt;In the short time I’ve been using it, I’ve found the interface to be clear and focused (without all the extra bloat Goodreads comes with). I was even able to import my Goodreads data (after exporting it first as a CSV). The website scales nicely so that it’s also highly usable on my phone’s browser.&lt;/p&gt;
&lt;p&gt;It feels safer to be using open-source software for managing another piece of my online data footprint, and it’s great to become more involved in the decentralised movement and community.&lt;/p&gt;
&lt;p&gt;I’ve already discovered some ineteresting “to-read” books. I look forward to exploring more over the coming months, and to continue seeing how the project develops.&lt;/p&gt;
</content:encoded>
    </item>
    
    <item>
      <title>Fixing up our outside space</title>
      <link>https://wilw.dev/blog/2021/08/14/garden-renovation/</link>
      <pubDate>Sat, 14 Aug 2021 15:02:00 +0000</pubDate>
      
      <guid>https://wilw.dev/blog/2021/08/14/garden-renovation/</guid>
      
        <category>100daystooffload</category>
      
        <category>life</category>
      
      
      <content:encoded>&lt;p&gt;About 18 months ago we bought a new home. The house is an 1880s (ish) Victorian building, and many of its original features - such as tile floors, cornice, and fireplaces - had been retained, which is great.&lt;/p&gt;
&lt;p&gt;It had previously been a student-style (HMO - house of multiple occupation) house, in which nearly all available space had been converted into bedrooms. As you might imagine, the fixtures and fittings weren’t in great conditions, with old and worn carpets and wallpaper throughout.&lt;/p&gt;
&lt;p&gt;Our plan was to try and restore the house back into a more family-style home. We knew this wouldn’t be a small undertaking (every room needed work to some degree). And this included the outside space.&lt;/p&gt;
&lt;p&gt;Our garden (or yard) isn’t massive (typical for a house in the city), measuring about 5x5 metres along with a side-return. In the decades past, the garden would have been used for access to the house for staff (cooks and cleaners, etc.) and for deliveries. As such, it featured raised beds and a mix of pathways throughout.&lt;/p&gt;
&lt;p&gt;When we first moved in, there was no flat or actual usable space, the beds had become overgrown, and it was full of junk left by the previous occupiers. Our plan was to flatten it into a practical, urban and low-maintenance space. We’d floor it with slabs and use nice pot plants to bring in some greenery.&lt;/p&gt;
&lt;p&gt;The rest of this post shows our progress!&lt;/p&gt;
&lt;h2 id=&#34;what-we-had-to-work-with-the-day-we-moved-in&#34;&gt;What we had to work with: the day we moved in&lt;/h2&gt;
&lt;p&gt;When we first moved in, the garden was full of overgrown and dead plants, and lots of junk.&lt;/p&gt;
&lt;p&gt;&lt;img src=&#34;https://wilw.dev/media/blog/garden1.jpg&#34; alt=&#34;A messy garden with junk everywhere&#34;&gt;
&lt;img src=&#34;https://wilw.dev/media/blog/garden2.jpg&#34; alt=&#34;More bins and junk&#34;&gt;
&lt;img src=&#34;https://wilw.dev/media/blog/garden3.jpg&#34; alt=&#34;Dead and overgrown raised beds&#34;&gt;&lt;/p&gt;
&lt;h2 id=&#34;flattening-the-garden&#34;&gt;Flattening the garden&lt;/h2&gt;
&lt;p&gt;We cleared lots of the junk and were able to flatten the beds. We used sledgehammers to knock out the low walls and raked the rubble and soil to level most of the area.&lt;/p&gt;
&lt;p&gt;&lt;img src=&#34;https://wilw.dev/media/blog/garden4.jpg&#34; alt=&#34;A (mostly) levelled space&#34;&gt;
&lt;img src=&#34;https://wilw.dev/media/blog/garden5.jpg&#34; alt=&#34;Still a few bits of junk&#34;&gt;&lt;/p&gt;
&lt;h2 id=&#34;slabs-foundation&#34;&gt;Slabs foundation&lt;/h2&gt;
&lt;p&gt;We began to lay some gravel as an eventual foundation for the slabs.&lt;/p&gt;
&lt;p&gt;&lt;img src=&#34;https://wilw.dev/media/blog/garden6.jpg&#34; alt=&#34;Laying gravel&#34;&gt;&lt;/p&gt;
&lt;h2 id=&#34;restoring-the-walls&#34;&gt;Restoring the walls&lt;/h2&gt;
&lt;p&gt;The walls looked grotty, as they were covered in peeling paint and plants. We cleared the plants and took a multi-tool to the walls to scrape off the old paint.&lt;/p&gt;
&lt;p&gt;&lt;img src=&#34;https://wilw.dev/media/blog/garden7.jpg&#34; alt=&#34;Scraping off the old paint&#34;&gt;&lt;/p&gt;
&lt;h2 id=&#34;fencing&#34;&gt;Fencing!&lt;/h2&gt;
&lt;p&gt;We knocked down (with our neighbour’s permission) the dividing fence between our gardens, and rebuilt a newer and sturdier one over a very rainy weekend.&lt;/p&gt;
&lt;p&gt;You can also see the more restored walls in the background and a new back gate that we built.&lt;/p&gt;
&lt;p&gt;&lt;img src=&#34;https://wilw.dev/media/blog/garden8.jpg&#34; alt=&#34;Laying the foundation for the fencing&#34;&gt;
&lt;img src=&#34;https://wilw.dev/media/blog/garden9.jpg&#34; alt=&#34;Getting things ready for the fences&#34;&gt;
&lt;img src=&#34;https://wilw.dev/media/blog/garden10.jpg&#34; alt=&#34;The new fence&#34;&gt;&lt;/p&gt;
&lt;p&gt;We also fenced in front of the wall on the other side.&lt;/p&gt;
&lt;p&gt;&lt;img src=&#34;https://wilw.dev/media/blog/garden11.jpg&#34; alt=&#34;Creating the other fence&#34;&gt;&lt;/p&gt;
&lt;h2 id=&#34;completing-the-floor&#34;&gt;Completing the floor&lt;/h2&gt;
&lt;p&gt;We laid the remainder of the slabs - almost two tons’ worth that we needed to carry through the house from the street. We pointed them and cleaned them up.&lt;/p&gt;
&lt;p&gt;You can see the other completed fence in the background.&lt;/p&gt;
&lt;p&gt;&lt;img src=&#34;https://wilw.dev/media/blog/garden12.jpg&#34; alt=&#34;Pointing the slabs&#34;&gt;&lt;/p&gt;
&lt;h2 id=&#34;finishing-touches&#34;&gt;Finishing touches&lt;/h2&gt;
&lt;p&gt;And that’s it, really. We put some new pots in, added some planks to create a sort of pergola, and some solar lights for the evening.&lt;/p&gt;
&lt;p&gt;We also cladded the little out-building (a utility room) with wood and repainted some of the brickwork.&lt;/p&gt;
&lt;p&gt;&lt;img src=&#34;https://wilw.dev/media/blog/garden13.jpg&#34; alt=&#34;The slabbed garden&#34;&gt;
&lt;img src=&#34;https://wilw.dev/media/blog/garden14.jpg&#34; alt=&#34;Some nice bunting&#34;&gt;
&lt;img src=&#34;https://wilw.dev/media/blog/garden15.jpg&#34; alt=&#34;Hanging plants&#34;&gt;&lt;/p&gt;
</content:encoded>
    </item>
    
    <item>
      <title>Treadl</title>
      <link>https://wilw.dev/blog/2021/08/13/treadl/</link>
      <pubDate>Fri, 13 Aug 2021 17:13:00 +0000</pubDate>
      
      <guid>https://wilw.dev/blog/2021/08/13/treadl/</guid>
      
        <category>100daystooffload</category>
      
        <category>technology</category>
      
        <category>project</category>
      
      
      <content:encoded>&lt;p&gt;I don’t tend to talk much about the &lt;a href=&#34;https://wilw.dev/projects&#34;&gt;projects&lt;/a&gt; I’m working on, but thought this would be a good opportunity to write a post about one such project - &lt;a href=&#34;https://treadl.com&#34; target=&#34;_blank&#34; rel=&#34;noopener noreferrer&#34; &gt;Treadl&lt;/a&gt;.&lt;/p&gt;
&lt;p&gt;Treadl is a web app (and more recently and less popularly a mobile app too). It enables &lt;a href=&#34;https://en.wikipedia.org/wiki/Weaving&#34; target=&#34;_blank&#34; rel=&#34;noopener noreferrer&#34; &gt;weavers&lt;/a&gt; to create and store their weaving patterns and projects online. This could be simply for personal use, or for sharing projects with others as a portfolio.&lt;/p&gt;
&lt;p&gt;&lt;img src=&#34;https://wilw.dev/media/blog/treadl1.png&#34; alt=&#34;Treadl&amp;rsquo;s project view, listing some files and weaving patterns&#34;&gt;&lt;/p&gt;
&lt;p&gt;The platform also allows for groups to be created, in which weavers can come together to discuss and share ideas (e.g. as part of a class or weaving group). Weavers can set up their individual profiles in order to tell others a little more about themselves. Users can also comment on other people’s projects and creations.&lt;/p&gt;
&lt;p&gt;Treadl is compatible with the Weaving Information File (WIF) file format standard. This means that patterns created in other weaving software (such as &lt;a href=&#34;http://www.fiberworks-pcw.com&#34; target=&#34;_blank&#34; rel=&#34;noopener noreferrer&#34; &gt;Fiberworks&lt;/a&gt;) can be imported into Treadl. Likewise, Treadl also allows for exporting patterns in WIF format to be imported into other compatible software.&lt;/p&gt;
&lt;p&gt;I myself am not a weaver. However, my mother is, and having a space where she and her different groups of weaving colleagues can design and store their patterns and drafts - along with engaging with each other socially - is useful. She initially had the idea for the platform and most of the feature ideas came from her!&lt;/p&gt;
&lt;p&gt;&lt;img src=&#34;https://wilw.dev/media/blog/treadl2.png&#34; alt=&#34;Treadl&amp;rsquo;s patten-editor screen&#34;&gt;&lt;/p&gt;
&lt;p&gt;Treadl is free to use as a means of giving back to the community. I had initially planned to create some paid-for features to help cover some of the ongoing hosting costs, but I have parked those ideas for now.&lt;/p&gt;
&lt;p&gt;The project is written in &lt;a href=&#34;https://reactjs.org&#34; target=&#34;_blank&#34; rel=&#34;noopener noreferrer&#34; &gt;React&lt;/a&gt; on the front-end, and Python on the back-end. The mobile app (which is mainly useful for the groups functionality) is written in &lt;a href=&#34;https://flutter.dev&#34; target=&#34;_blank&#34; rel=&#34;noopener noreferrer&#34; &gt;Flutter&lt;/a&gt;.&lt;/p&gt;
</content:encoded>
    </item>
    
    <item>
      <title>Pinephone update: the first few weeks</title>
      <link>https://wilw.dev/blog/2021/08/09/pinephone-first-few-weeks/</link>
      <pubDate>Mon, 09 Aug 2021 17:51:00 +0000</pubDate>
      
      <guid>https://wilw.dev/blog/2021/08/09/pinephone-first-few-weeks/</guid>
      
        <category>100daystooffload</category>
      
        <category>technology</category>
      
        <category>pinephone</category>
      
      
      <content:encoded>&lt;p&gt;&lt;a href=&#34;https://wilw.dev/blog/2021/04/27/pinephone&#34;&gt;Back in April&lt;/a&gt;, I bought a &lt;a href=&#34;https://wiki.pine64.org/index.php/PinePhone&#34; target=&#34;_blank&#34; rel=&#34;noopener noreferrer&#34; &gt;Pinephone&lt;/a&gt;. I used the phone quite consistently for the first few weeks and I meant to write an update here a couple of months back, but work (and &lt;a href=&#34;https://wilw.dev/blog/2021/06/10/married&#34;&gt;other things&lt;/a&gt;) got in the way a bit.&lt;/p&gt;
&lt;p&gt;So, here is my delayed “first few weeks with a Pinephone” update.&lt;/p&gt;
&lt;p&gt;As mentioned, I initially aimed to simply use the phone in its out-of-the-box state (i.e. Manjaro Linux with KDE Plasma Mobile) - not as a daily driver, but more as a means of measuring the phone’s base case usability. However, hopefully with an aim to eventually being able to use such a device more full-time.&lt;/p&gt;
&lt;h2 id=&#34;overall-impression&#34;&gt;Overall impression&lt;/h2&gt;
&lt;p&gt;Generally, I’ve really enjoyed using the Pinephone. I mean, it is slow (compared to other modern phones), apps take a good few seconds to open, and the UI freezes every now and again. However, things are moving quickly - there are other OS builds and desktop environments available to try.&lt;/p&gt;
&lt;p&gt;It is encouraging to see successes from the base shipped version of the device. For example, below is a picture of my dog that I took today using the Pinephone.&lt;/p&gt;
&lt;p&gt;&lt;img src=&#34;https://wilw.dev/media/blog/pinephone_b1.jpg&#34; alt=&#34;A picture of my dog - taken on Pinephone&#34;&gt;&lt;/p&gt;
&lt;h2 id=&#34;what-did-i-try&#34;&gt;What did I try?&lt;/h2&gt;
&lt;p&gt;In my &lt;a href=&#34;https://wilw.dev/blog/2021/04/27/pinephone&#34;&gt;previous post&lt;/a&gt;, I outlined a few areas and day-to-day tasks I wanted to try and accomplish. I’ll evaluate against each of them (so far!) below.&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;Basic calls and texts:&lt;/strong&gt; ☑️ worked out of the box. The Pinephone accepted my SIM card and connected without issue.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;4G cellular connectivity:&lt;/strong&gt; ☑️ worked out of the box. Drops out every now and again, requiring a reconnect.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;WiFi connectivity:&lt;/strong&gt; ☑️ worked out of the box, automatically connects.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Bluetooth connectivity (including headphones):&lt;/strong&gt; ☑️ worked without issue, was able to connect and play through my Airpods Pro. Airpods-initiated pause/play also worked!&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Photo- and video-taking using both front- and rear-facing cameras:&lt;/strong&gt; ☑️ ⚠️ taking photos works fine using the &lt;a href=&#34;https://git.sr.ht/~martijnbraam/megapixels&#34; target=&#34;_blank&#34; rel=&#34;noopener noreferrer&#34; &gt;Megapixels&lt;/a&gt; app (though does require an app restart for the camera to activate properly), videos not yet present.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Web browsing:&lt;/strong&gt; ☑️ works great, using the shipped &lt;a href=&#34;https://apps.kde.org/en-gb/angelfish&#34; target=&#34;_blank&#34; rel=&#34;noopener noreferrer&#34; &gt;Angelfish&lt;/a&gt; browser.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Podcast subscribing, listing, and listening:&lt;/strong&gt; ☑️ Works great using the &lt;a href=&#34;https://flathub.org/apps/details/org.gnome.Podcasts&#34; target=&#34;_blank&#34; rel=&#34;noopener noreferrer&#34; &gt;Podcasts&lt;/a&gt; app. Could subscribe to all my podcasts. Only thing I couldn’t get working is changing the playback speed (though this I think is just a UI bug as the setting is there).&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Audiobook downloading and listening:&lt;/strong&gt; ☑️ I use Audible still as I am not aware of a good DRM-free audiobook retailer, however there are tools to convert the Audible AAX files you own to MP3 for DRM-free playback. &lt;a href=&#34;https://github.com/KrumpetPirate/AAXtoMP3&#34; target=&#34;_blank&#34; rel=&#34;noopener noreferrer&#34; &gt;AAXtoMP3&lt;/a&gt; looks like it does the trick.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Music-playing (preferably through Spotify):&lt;/strong&gt; ☑️ Works fine using the &lt;a href=&#34;https://flathub.org/apps/details/dev.alextren.Spot&#34; target=&#34;_blank&#34; rel=&#34;noopener noreferrer&#34; &gt;Spot&lt;/a&gt; app. Can access playlists and play music (ironically the Spot app is more responsive than the Spotify app on my iPhone!).&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Mastodon (tooting and reading my timelines):&lt;/strong&gt; ⚠️ &lt;a href=&#34;https://flathub.org/apps/details/com.github.bleakgrey.tootle&#34; target=&#34;_blank&#34; rel=&#34;noopener noreferrer&#34; &gt;Tootle&lt;/a&gt; looks great, but I have been unable to fully use it. (I can’t for the life of me get copy/paste to work properly, and so I cannot copy the authorization code to login). But it works via the web.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;RSS (viewing my feeds from my FreshRSS server):&lt;/strong&gt; ☑️ Working fine using the &lt;a href=&#34;https://flathub.org/apps/details/com.gitlab.newsflash&#34; target=&#34;_blank&#34; rel=&#34;noopener noreferrer&#34; &gt;NewsFlash&lt;/a&gt; app (connected via the &lt;a href=&#34;https://feedafever.com/api&#34; target=&#34;_blank&#34; rel=&#34;noopener noreferrer&#34; &gt;Fever API&lt;/a&gt; to my existing &lt;a href=&#34;https://www.freshrss.org&#34; target=&#34;_blank&#34; rel=&#34;noopener noreferrer&#34; &gt;FreshRSS&lt;/a&gt; server).&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Email reading and sending:&lt;/strong&gt; ☑️ worked OK using &lt;a href=&#34;https://flathub.org/apps/details/org.gnome.Geary&#34; target=&#34;_blank&#34; rel=&#34;noopener noreferrer&#34; &gt;Geary&lt;/a&gt;. Unable to login to my Google Mail accounts, but others worked.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Telegram messaging:&lt;/strong&gt; ☑️ Worked with no problems.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Password-management:&lt;/strong&gt; ⚠️ Could not get working: the available Bitwarden app does not scale small enough to enable me to login. But promising it exists in the store.&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;&lt;img src=&#34;https://wilw.dev/media/blog/pinephone_b2.png&#34; alt=&#34;A screenshot of the Podcasts app listing some of my subscriptions&#34;&gt;
&lt;strong&gt;&lt;em&gt;Above: a screenshot from the Podcasts app listing some of my subscriptions.&lt;/em&gt;&lt;/strong&gt;&lt;/p&gt;
&lt;h2 id=&#34;moving-forward&#34;&gt;Moving forward&lt;/h2&gt;
&lt;p&gt;Clearly the Pinephone is capable of fulfilling most of my every-day mobile device needs - right out of the box. As I say, the device is a little slow and crash-prone (&lt;em&gt;currently&lt;/em&gt;) to use fully as a daily driver. Additionally, the issue I’m having with Bitwarden and copy/paste makes accessing some services tricky.&lt;/p&gt;
&lt;p&gt;However, this certainly feels like it’s just the start. I am excited to give &lt;a href=&#34;https://puri.sm/posts/phosh-overview&#34; target=&#34;_blank&#34; rel=&#34;noopener noreferrer&#34; &gt;Phosh&lt;/a&gt; a go and to get involved further in the &lt;a href=&#34;https://flathub.org&#34; target=&#34;_blank&#34; rel=&#34;noopener noreferrer&#34; &gt;Flathub&lt;/a&gt; ecosystem to see if I can improve on the functionality and usability. I’ll update again soon!&lt;/p&gt;
</content:encoded>
    </item>
    
    <item>
      <title>My development stack</title>
      <link>https://wilw.dev/blog/2021/08/04/tech-stack/</link>
      <pubDate>Wed, 04 Aug 2021 18:56:00 +0000</pubDate>
      
      <guid>https://wilw.dev/blog/2021/08/04/tech-stack/</guid>
      
        <category>100daystooffload</category>
      
        <category>technology</category>
      
      
      <content:encoded>&lt;p&gt;Some people have complex development processes and flows - making use of tools such as heavy editors and IDEs, Docker for running and building locally in development, or even develop entirely remotely over SSH connections. Other people use simpler combinations of tools.&lt;/p&gt;
&lt;p&gt;I thought I’d write briefly about what I use on a daily basis. I have a relatively simple development tech stack:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;Terminal application (I use the &lt;code&gt;Terminal.app&lt;/code&gt; application that ships with my Mac, since this works best for me)&lt;/li&gt;
&lt;li&gt;&lt;a href=&#34;https://github.com/tmux/tmux/wiki&#34; target=&#34;_blank&#34; rel=&#34;noopener noreferrer&#34; &gt;tmux&lt;/a&gt; - for handling multiple windows and panes&lt;/li&gt;
&lt;li&gt;&lt;a href=&#34;https://github.com/tmuxinator/tmuxinator&#34; target=&#34;_blank&#34; rel=&#34;noopener noreferrer&#34; &gt;tmuxinator&lt;/a&gt; - for managing complex tmux sessions (I recently &lt;a href=&#34;https://wilw.dev/blog/2021/06/18/tmuxinator&#34;&gt;wrote more about this&lt;/a&gt;)&lt;/li&gt;
&lt;li&gt;&lt;a href=&#34;https://www.vim.org&#34; target=&#34;_blank&#34; rel=&#34;noopener noreferrer&#34; &gt;vim&lt;/a&gt; - a simple yet powerful text editor&lt;/li&gt;
&lt;li&gt;&lt;a href=&#34;https://git-scm.com&#34; target=&#34;_blank&#34; rel=&#34;noopener noreferrer&#34; &gt;git&lt;/a&gt; - for source control&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;I also use a small number of Vim plugins - installed via &lt;a href=&#34;https://github.com/VundleVim/Vundle.vim&#34; target=&#34;_blank&#34; rel=&#34;noopener noreferrer&#34; &gt;Vundle&lt;/a&gt; - to add nice quality-of-life features to my editor:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;code&gt;pangloss/vim-javascript&lt;/code&gt; - better JavaScript syntax highlighting&lt;/li&gt;
&lt;li&gt;&lt;code&gt;mxw/vim-jsx&lt;/code&gt; - JSX syntax highlighting&lt;/li&gt;
&lt;li&gt;&lt;code&gt;dart-lang/dart-vim-plugin&lt;/code&gt; - Dart language syntax highlighting&lt;/li&gt;
&lt;li&gt;&lt;code&gt;rust-lang/rust.vim&lt;/code&gt; - Rust language syntax highlighting&lt;/li&gt;
&lt;li&gt;&lt;code&gt;morhetz/gruvbox&lt;/code&gt; - Attractive Vim colour scheme&lt;/li&gt;
&lt;li&gt;&lt;code&gt;airblade/vim-gitgutter&lt;/code&gt; - In-editor Git indicators&lt;/li&gt;
&lt;li&gt;&lt;code&gt;ctrlpvim/ctrlp.vim&lt;/code&gt; - Awesome and quick file searching&lt;/li&gt;
&lt;li&gt;&lt;code&gt;scrooloose/nerdtree&lt;/code&gt; - Easy file/directory tree navigation&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;I backup and sync my dotfiles (e.g. my &lt;code&gt;.vimrc&lt;/code&gt; and &lt;code&gt;.tmux.conf&lt;/code&gt;) by symlinking from my Nextcloud sync directory.&lt;/p&gt;
&lt;p&gt;And that’s it, really. In terms of per-project management, I make use of native tooling depending on the languages and frameworks being used. For example, for Node/JavaScript projects I use &lt;code&gt;yarn&lt;/code&gt; and for Python projects I use &lt;code&gt;virtualenv&lt;/code&gt;s and &lt;a href=&#34;https://python-poetry.org&#34; target=&#34;_blank&#34; rel=&#34;noopener noreferrer&#34; &gt;Poetry&lt;/a&gt; for handling dependencies.&lt;/p&gt;
&lt;p&gt;It’d be nice to have an elegant cross-platform approach to provisioning new machines with this setup, but given its relative simplicity it isn’t too much of a headache to get things back up and running again when I switch machines!&lt;/p&gt;
</content:encoded>
    </item>
    
    <item>
      <title>Our first few months with a dog</title>
      <link>https://wilw.dev/blog/2021/08/03/dog/</link>
      <pubDate>Tue, 03 Aug 2021 18:59:00 +0000</pubDate>
      
      <guid>https://wilw.dev/blog/2021/08/03/dog/</guid>
      
        <category>100daystooffload</category>
      
        <category>life</category>
      
      
      <content:encoded>&lt;p&gt;About nine months ago - at the end of November last year - we adopted a dog. Although I’ve always grown up with and around dogs owned by parents and siblings, I’ve never been a huge “dog person” myself. However, it is very easy to get attached very quickly!&lt;/p&gt;
&lt;p&gt;&lt;img src=&#34;https://wilw.dev/media/blog/dog1.jpg&#34; alt=&#34;Picture of my dog, a chocolate cocker spaniel&#34;&gt;&lt;/p&gt;
&lt;p&gt;The dog we adopted is an English working cocker spaniel. As you might imagine, he is extremely energetic. He’s a nightmare on (and sometimes off) his lead, and his recall isn’t fantastic yet due to us needing to re-train him after his previous owner.&lt;/p&gt;
&lt;p&gt;However, he is lovely to have around the house. He’s great company for me when working from home and still always follows me wherever I go. He sprints around the park for hours on end with his dog friends but is super calm and relaxed when at home, which is great. He’s always extra cuddly in the evenings, but hates having to go to his own bed when it’s time to go to sleep.&lt;/p&gt;
&lt;p&gt;&lt;img src=&#34;https://wilw.dev/media/blog/dog2.jpg&#34; alt=&#34;Another picture!&#34;&gt;&lt;/p&gt;
&lt;p&gt;He gets me out the house even on rainy days, and I’ve met and made friends with lots of other dog owners in the parks near where we live.&lt;/p&gt;
&lt;p&gt;It’s only been a few months, but he is definitely already a big part of our family. It’s hard to now imagine life without him!&lt;/p&gt;
</content:encoded>
    </item>
    
    <item>
      <title>Code syntax highlighting in Gatsby</title>
      <link>https://wilw.dev/blog/2021/07/28/gatsby-syntax-highlighting/</link>
      <pubDate>Wed, 28 Jul 2021 17:59:00 +0000</pubDate>
      
      <guid>https://wilw.dev/blog/2021/07/28/gatsby-syntax-highlighting/</guid>
      
        <category>100daystooffload</category>
      
        <category>technology</category>
      
        <category>javascript</category>
      
        <category>react</category>
      
      
      <content:encoded>&lt;p&gt;Providing code snippets on your website or blog can be a great way to convey meaning for technical concepts.&lt;/p&gt;
&lt;p&gt;Using the HTML &lt;code&gt;pre&lt;/code&gt; tag can help provide structure to your listings, in terms of spacing and indentation, but highlighting keywords - as most people do in their code text editors - also vastly helps improve readability.&lt;/p&gt;
&lt;p&gt;For example, consider the below JavaScript snippet.&lt;/p&gt;
&lt;pre tabindex=&#34;0&#34;&gt;&lt;code&gt;class Greeter {
  greet(name) {
    console.log(`Hello, ${name}!`);
  }
}

const greeter = new Greeter();
greeter.greet(&#39;Will&#39;);
&lt;/code&gt;&lt;/pre&gt;&lt;p&gt;The exact same listing is displayed below with some simple syntax highlighting. The structure and meaning of the code becomes much easier to understand.&lt;/p&gt;
&lt;div class=&#34;highlight&#34;&gt;&lt;pre tabindex=&#34;0&#34; style=&#34;background-color:#f0f3f3;-moz-tab-size:2;-o-tab-size:2;tab-size:2;&#34;&gt;&lt;code class=&#34;language-javascript&#34; data-lang=&#34;javascript&#34;&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;&lt;span style=&#34;color:#069;font-weight:bold&#34;&gt;class&lt;/span&gt; Greeter {
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;  greet(name) {
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;    console.log(&lt;span style=&#34;color:#c30&#34;&gt;`Hello, &lt;/span&gt;&lt;span style=&#34;color:#a00&#34;&gt;${&lt;/span&gt;name&lt;span style=&#34;color:#a00&#34;&gt;}&lt;/span&gt;&lt;span style=&#34;color:#c30&#34;&gt;!`&lt;/span&gt;);
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;  }
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;}
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;&lt;span style=&#34;color:#069;font-weight:bold&#34;&gt;const&lt;/span&gt; greeter &lt;span style=&#34;color:#555&#34;&gt;=&lt;/span&gt; &lt;span style=&#34;color:#069;font-weight:bold&#34;&gt;new&lt;/span&gt; Greeter();
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;greeter.greet(&lt;span style=&#34;color:#c30&#34;&gt;&#39;Will&#39;&lt;/span&gt;);
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;If you build your website with &lt;a href=&#34;https://www.gatsbyjs.com&#34; target=&#34;_blank&#34; rel=&#34;noopener noreferrer&#34; &gt;GatsbyJS&lt;/a&gt; and write your blog posts or pages in markdown format, then adding syntax highlighting is super easy.&lt;/p&gt;
&lt;p&gt;&lt;a href=&#34;https://prismjs.com&#34; target=&#34;_blank&#34; rel=&#34;noopener noreferrer&#34; &gt;PrismJS&lt;/a&gt; is a well-established library for achieving syntax highlighting on any website. Luckily, there is also a &lt;a href=&#34;https://www.gatsbyjs.com/plugins/gatsby-remark-prismjs&#34; target=&#34;_blank&#34; rel=&#34;noopener noreferrer&#34; &gt;Gatsby plugin&lt;/a&gt; for Prism which makes it much easier to get started in Gatsby websites.&lt;/p&gt;
&lt;p&gt;First of all, add the required dependencies to your project:&lt;/p&gt;
&lt;div class=&#34;highlight&#34;&gt;&lt;pre tabindex=&#34;0&#34; style=&#34;background-color:#f0f3f3;-moz-tab-size:2;-o-tab-size:2;tab-size:2;&#34;&gt;&lt;code class=&#34;language-shell&#34; data-lang=&#34;shell&#34;&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;yarn add gatsby-transformer-remark gatsby-remark-prismjs prismjs
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;Next, add an entry to the &lt;code&gt;plugins&lt;/code&gt; array in your &lt;code&gt;gatsby-config.js&lt;/code&gt; file to use the plugin. For example, the below is what I use.&lt;/p&gt;
&lt;div class=&#34;highlight&#34;&gt;&lt;pre tabindex=&#34;0&#34; style=&#34;background-color:#f0f3f3;-moz-tab-size:2;-o-tab-size:2;tab-size:2;&#34;&gt;&lt;code class=&#34;language-javascript&#34; data-lang=&#34;javascript&#34;&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;  ...
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;  plugins&lt;span style=&#34;color:#555&#34;&gt;:&lt;/span&gt; [
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;    ...
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;    {
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;      resolve&lt;span style=&#34;color:#555&#34;&gt;:&lt;/span&gt; &lt;span style=&#34;color:#c30&#34;&gt;&#39;gatsby-transformer-remark&#39;&lt;/span&gt;,
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;      options&lt;span style=&#34;color:#555&#34;&gt;:&lt;/span&gt; {
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;        plugins&lt;span style=&#34;color:#555&#34;&gt;:&lt;/span&gt; [
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;          {
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;            resolve&lt;span style=&#34;color:#555&#34;&gt;:&lt;/span&gt; &lt;span style=&#34;color:#c30&#34;&gt;&#39;gatsby-remark-prismjs&#39;&lt;/span&gt;,
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;            options&lt;span style=&#34;color:#555&#34;&gt;:&lt;/span&gt; {
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;              classPrefix&lt;span style=&#34;color:#555&#34;&gt;:&lt;/span&gt; &lt;span style=&#34;color:#c30&#34;&gt;&#39;language-&#39;&lt;/span&gt;,
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;              showLineNumbers&lt;span style=&#34;color:#555&#34;&gt;:&lt;/span&gt; &lt;span style=&#34;color:#069;font-weight:bold&#34;&gt;true&lt;/span&gt;,
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;              noInlineHighlight&lt;span style=&#34;color:#555&#34;&gt;:&lt;/span&gt; &lt;span style=&#34;color:#069;font-weight:bold&#34;&gt;false&lt;/span&gt;,
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;            }
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;          }
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;        ]
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;      },
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;    },
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;  ],
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;  ...
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;Finally, you’ll need to choose a theme. There are a number of &lt;a href=&#34;https://github.com/PrismJS/prism/tree/master/themes&#34; target=&#34;_blank&#34; rel=&#34;noopener noreferrer&#34; &gt;official ones available&lt;/a&gt; from Prism. To enable one, simply add an entry to your &lt;code&gt;gatsby-browser.js&lt;/code&gt; file. For example:&lt;/p&gt;
&lt;div class=&#34;highlight&#34;&gt;&lt;pre tabindex=&#34;0&#34; style=&#34;background-color:#f0f3f3;-moz-tab-size:2;-o-tab-size:2;tab-size:2;&#34;&gt;&lt;code class=&#34;language-javascript&#34; data-lang=&#34;javascript&#34;&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;require(&lt;span style=&#34;color:#c30&#34;&gt;&#39;prismjs/themes/prism-solarizedlight.css&#39;&lt;/span&gt;);
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;That’s it - now in your markdown you can add a block of code to syntax-highlight as follows:&lt;/p&gt;
&lt;!-- raw HTML omitted --&gt;
&lt;!-- raw HTML omitted --&gt;
&lt;p&gt;You can change the &lt;code&gt;javascript&lt;/code&gt; part to a &lt;a href=&#34;https://prismjs.com/#supported-languages&#34; target=&#34;_blank&#34; rel=&#34;noopener noreferrer&#34; &gt;supported language&lt;/a&gt; you are using, and Prism will syntax-highlight accordingly.&lt;/p&gt;
&lt;h2 id=&#34;extra-configuration&#34;&gt;Extra configuration&lt;/h2&gt;
&lt;p&gt;If you want to add extra features - such as line numbers - or if you run into issues, I recommend checking out &lt;a href=&#34;https://www.gatsbyjs.com/plugins/gatsby-remark-prismjs&#34; target=&#34;_blank&#34; rel=&#34;noopener noreferrer&#34; &gt;the documentation for the plugin&lt;/a&gt;.&lt;/p&gt;
</content:encoded>
    </item>
    
    <item>
      <title>Capsule.Town</title>
      <link>https://wilw.dev/blog/2021/07/27/capsule-town/</link>
      <pubDate>Tue, 27 Jul 2021 17:55:00 +0000</pubDate>
      
      <guid>https://wilw.dev/blog/2021/07/27/capsule-town/</guid>
      
        <category>100daystooffload</category>
      
        <category>technology</category>
      
        <category>gemini</category>
      
        <category>project</category>
      
      
      <content:encoded>&lt;p&gt;The Gemini protocol has gathered even more momentum in the few months since I &lt;a href=&#34;https://wilw.dev/blog/2021/01/20/project-gemini&#34;&gt;last posted about it&lt;/a&gt;.&lt;/p&gt;
&lt;p&gt;Its popularity is largely driven by its privacy-focused and content-oriented design. It doesn’t allow for bloated sites or resource-hungry client-side scripting. It’s a means for simply and easily accessing content that is useful to you - either by hosting a capsule yourself or by joining an existing community.&lt;/p&gt;
&lt;p&gt;In this post I am introducing Capsule.Town - a way in which I can try and give back to the FOSS community.&lt;/p&gt;
&lt;p&gt;Capsule.Town (accessible using your Gem client by visiting &lt;a href=&#34;gemini://capsule.town&#34;&gt;gemini://capsule.town&lt;/a&gt; or through your web browser via a &lt;a href=&#34;https://portal.mozz.us/gemini/capsule.town&#34; target=&#34;_blank&#34; rel=&#34;noopener noreferrer&#34; &gt;web proxy service&lt;/a&gt;) is a means for easily and safely hosting your Gemini content.&lt;/p&gt;
&lt;p&gt;&lt;img src=&#34;https://wilw.dev/media/blog/capsuletown.png&#34; alt=&#34;Capsule.Town logo&#34;&gt;&lt;/p&gt;
&lt;p&gt;It’s a free service for publishing your own Gemini capsule. Currently this gives you a subdomain at the &lt;code&gt;capsule.town&lt;/code&gt; domain, but I hope to later add support for bringing your own domain. No registration is required, meaning you can get started straight away without needing an email address or username.&lt;/p&gt;
&lt;p&gt;All of the code is &lt;a href=&#34;https://git.wilw.dev/wilw/capsule-town&#34; target=&#34;_blank&#34; rel=&#34;noopener noreferrer&#34; &gt;open-source&lt;/a&gt; and distributed under the BSD 2-clause license. This is aside from the Gem server itself, which is built on top of the &lt;a href=&#34;https://github.com/mbrubeck/agate&#34; target=&#34;_blank&#34; rel=&#34;noopener noreferrer&#34; &gt;Agate project&lt;/a&gt;.&lt;/p&gt;
&lt;p&gt;The recommended approach is to download the binary for your system, and use this to publish your capsule. To get started, visit &lt;a href=&#34;gemini://capsule.town/start.gmi&#34;&gt;gemini://capsule.town/start.gmi&lt;/a&gt; (or the &lt;a href=&#34;https://portal.mozz.us/gemini/capsule.town/start.gmi&#34; target=&#34;_blank&#34; rel=&#34;noopener noreferrer&#34; &gt;equivalent via the web&lt;/a&gt;) and follow the instructions.&lt;/p&gt;
&lt;p&gt;Whilst other free services exist that also allow you to host Gemini pages, the aim with Capsule.Town is to create a Netlify-like process for quick and easy publication of content.&lt;/p&gt;
&lt;p&gt;Feel free to give it a try. If you have any questions or problems, just get in touch.&lt;/p&gt;
</content:encoded>
    </item>
    
    <item>
      <title>Accidental Tech Podcast</title>
      <link>https://wilw.dev/blog/2021/07/26/atp/</link>
      <pubDate>Mon, 26 Jul 2021 19:27:00 +0000</pubDate>
      
      <guid>https://wilw.dev/blog/2021/07/26/atp/</guid>
      
        <category>100daystooffload</category>
      
        <category>technology</category>
      
        <category>podcast</category>
      
      
      <content:encoded>&lt;p&gt;I listen to a number of podcasts each week. One of these is &lt;a href=&#34;https://atp.fm&#34; target=&#34;_blank&#34; rel=&#34;noopener noreferrer&#34; &gt;ATP (Accidental Tech Podcast)&lt;/a&gt;.&lt;/p&gt;
&lt;p&gt;&lt;img src=&#34;https://wilw.dev/media/blog/atp.png&#34; alt=&#34;ATP logo&#34;&gt;&lt;/p&gt;
&lt;p&gt;This is one of my favourite weekly podcasts. It’s humurous and full of cutting-edge discussion from the tech world, and I always look forward to new episodes.&lt;/p&gt;
&lt;p&gt;The episodes are primarily Apple focused, which is fine for me since I’m a big user of Apple products. Some episodes are more technical than others - discussing programming and development approaches - whilst others are focused more on user-facing items.&lt;/p&gt;
&lt;p&gt;Episodes also include lots of informal chit-chat about general tech (electric vehicles, drones, networks, and more). It’s easy listening, and perfect for when out walking or exercising.&lt;/p&gt;
&lt;p&gt;I can definitely recommend trying it out. It should come up in your podcast app if you search for “ATP”. I use &lt;a href=&#34;https://overcast.fm&#34; target=&#34;_blank&#34; rel=&#34;noopener noreferrer&#34; &gt;Overcast&lt;/a&gt; on iOS (which was created by one of the ATP hosts), and if you do also you can subscribe at &lt;a href=&#34;https://overcast.fm/itunes617416468&#34; target=&#34;_blank&#34; rel=&#34;noopener noreferrer&#34; &gt;this link&lt;/a&gt;.&lt;/p&gt;
&lt;p&gt;There is also an option to become an &lt;a href=&#34;https://atp.fm/join&#34; target=&#34;_blank&#34; rel=&#34;noopener noreferrer&#34; &gt;ATP member&lt;/a&gt;, which gives you unedited and ad-free versions of the show (along with other benefits).&lt;/p&gt;
</content:encoded>
    </item>
    
    <item>
      <title>Generating video previews in webapps</title>
      <link>https://wilw.dev/blog/2021/07/19/video-previews/</link>
      <pubDate>Mon, 19 Jul 2021 16:04:00 +0000</pubDate>
      
      <guid>https://wilw.dev/blog/2021/07/19/video-previews/</guid>
      
        <category>100daystooffload</category>
      
        <category>technology</category>
      
        <category>javascript</category>
      
      
      <content:encoded>&lt;p&gt;Many web apps have support for uploading video files. Whether it’s a media-focused platform (such as a video sharing service) or just offering users a chance to add vlogs to their profile - videos are a powerful mechanism for distributing ideas.&lt;/p&gt;
&lt;p&gt;For services providing image upload functionality, it is relatively simple to build in processes that extract smaller versions of the files (e.g. thumbnails) to be used as image previews. This allows other users to see roughly what an image is about before opening a larger version. It also enables more interesting, responsive, and attractive interfaces - since the smaller images can be loaded more quickly.&lt;/p&gt;
&lt;p&gt;For videos, however, the process is less obvious. Particularly when using the native &lt;code&gt;video&lt;/code&gt; tag, on some browsers often just an empty black rectangle is displayed before the video begins playing. This can result in uglier interfaces in which users cannot get a preview before the video plays.&lt;/p&gt;
&lt;p&gt;However, there is a way in which video preview thumbnails can be extracted from your videos. These can then be stored alongside your video and displayed in-place of the video on a webpage to provide context about the video before it plays.&lt;/p&gt;
&lt;p&gt;This process can be completed as part of the video selection process on the user’s device, so no extra server-side processing is required. It essentially involves “playing” the video in the background in a hidden player for a second or two, and then extracting the current frame as an image to be stored.&lt;/p&gt;
&lt;p&gt;The code below should help explain the process in more detail.&lt;/p&gt;
&lt;div class=&#34;highlight&#34;&gt;&lt;pre tabindex=&#34;0&#34; style=&#34;background-color:#f0f3f3;-moz-tab-size:2;-o-tab-size:2;tab-size:2;&#34;&gt;&lt;code class=&#34;language-javascript&#34; data-lang=&#34;javascript&#34;&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;&lt;span style=&#34;color:#069;font-weight:bold&#34;&gt;const&lt;/span&gt; getVideoPreview &lt;span style=&#34;color:#555&#34;&gt;=&lt;/span&gt; (file, time) =&gt; {
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;    &lt;span style=&#34;color:#069;font-weight:bold&#34;&gt;return&lt;/span&gt; &lt;span style=&#34;color:#069;font-weight:bold&#34;&gt;new&lt;/span&gt; &lt;span style=&#34;color:#366&#34;&gt;Promise&lt;/span&gt;((resolve, reject) =&gt; {
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;        &lt;span style=&#34;color:#09f;font-style:italic&#34;&gt;// create a video player
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;&lt;span style=&#34;color:#09f;font-style:italic&#34;&gt;&lt;/span&gt;        &lt;span style=&#34;color:#069;font-weight:bold&#34;&gt;const&lt;/span&gt; player &lt;span style=&#34;color:#555&#34;&gt;=&lt;/span&gt; &lt;span style=&#34;color:#366&#34;&gt;document&lt;/span&gt;.createElement(&lt;span style=&#34;color:#c30&#34;&gt;&#39;video&#39;&lt;/span&gt;);
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;        &lt;span style=&#34;color:#09f;font-style:italic&#34;&gt;// when the metadata loads…
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;&lt;span style=&#34;color:#09f;font-style:italic&#34;&gt;&lt;/span&gt;        player.addEventListener(&lt;span style=&#34;color:#c30&#34;&gt;&#39;loadedmetadata&#39;&lt;/span&gt;, () =&gt; {
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;            &lt;span style=&#34;color:#09f;font-style:italic&#34;&gt;// when the video has seeked…
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;&lt;span style=&#34;color:#09f;font-style:italic&#34;&gt;&lt;/span&gt;            player.addEventListener(&lt;span style=&#34;color:#c30&#34;&gt;&#39;seeked&#39;&lt;/span&gt;, () =&gt; {
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;                &lt;span style=&#34;color:#09f;font-style:italic&#34;&gt;// create canvas and draw current frame
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;&lt;span style=&#34;color:#09f;font-style:italic&#34;&gt;&lt;/span&gt;                &lt;span style=&#34;color:#069;font-weight:bold&#34;&gt;const&lt;/span&gt; canvas &lt;span style=&#34;color:#555&#34;&gt;=&lt;/span&gt; &lt;span style=&#34;color:#366&#34;&gt;document&lt;/span&gt;.createElement(&lt;span style=&#34;color:#c30&#34;&gt;&#39;canvas&#39;&lt;/span&gt;);
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;                canvas.width &lt;span style=&#34;color:#555&#34;&gt;=&lt;/span&gt; player.videoWidth;
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;                canvas.height &lt;span style=&#34;color:#555&#34;&gt;=&lt;/span&gt; player.videoHeight;
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;                &lt;span style=&#34;color:#069;font-weight:bold&#34;&gt;const&lt;/span&gt; ctx &lt;span style=&#34;color:#555&#34;&gt;=&lt;/span&gt; canvas.getContext(&lt;span style=&#34;color:#c30&#34;&gt;&#39;2d&#39;&lt;/span&gt;);
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;                ctx.drawImage(player, &lt;span style=&#34;color:#f60&#34;&gt;0&lt;/span&gt;, &lt;span style=&#34;color:#f60&#34;&gt;0&lt;/span&gt;, canvas.width, canvas.height);
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;                &lt;span style=&#34;color:#09f;font-style:italic&#34;&gt;// resolve with image file
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;&lt;span style=&#34;color:#09f;font-style:italic&#34;&gt;&lt;/span&gt;                ctx.canvas.toBlob(resolve, &lt;span style=&#34;color:#c30&#34;&gt;&#39;image/jpeg&#39;&lt;/span&gt;, &lt;span style=&#34;color:#f60&#34;&gt;0.75&lt;/span&gt;);
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;            });
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;            player.currentTime &lt;span style=&#34;color:#555&#34;&gt;=&lt;/span&gt; time;
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;        });
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;        player.setAttribute(&lt;span style=&#34;color:#c30&#34;&gt;&#39;src&#39;&lt;/span&gt;, URL.createObjectURL(file));
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;        player.load();
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;    });
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;  }
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;Elsewhere in your code, you could now call &lt;code&gt;const videoPreview = await getVideoPreview(file, 1);&lt;/code&gt; in order to get an image file representing the video after playing for one second. Then both &lt;code&gt;file&lt;/code&gt; (the video selected by the user from the filesystem) and &lt;code&gt;videoPreview&lt;/code&gt; (the preview image) can be separately uploaded.&lt;/p&gt;
&lt;p&gt;When you then want to show the video preview, you can simply load the image file and then begin to play the video once it is selected by the user.&lt;/p&gt;
</content:encoded>
    </item>
    
    <item>
      <title>The Night Circus by Erin Morgenstern</title>
      <link>https://wilw.dev/blog/2021/07/14/the-night-circus/</link>
      <pubDate>Wed, 14 Jul 2021 17:30:00 +0000</pubDate>
      
      <guid>https://wilw.dev/blog/2021/07/14/the-night-circus/</guid>
      
        <category>100daystooffload</category>
      
        <category>book</category>
      
      
      <content:encoded>&lt;p&gt;Every now and again it’s nice to dive back into a young adult book. I recently read &lt;a href=&#34;https://www.goodreads.com/book/show/9361589-the-night-circus&#34; target=&#34;_blank&#34; rel=&#34;noopener noreferrer&#34; &gt;The Night Circus&lt;/a&gt; by &lt;a href=&#34;https://www.goodreads.com/author/show/4370565.Erin_Morgenstern&#34; target=&#34;_blank&#34; rel=&#34;noopener noreferrer&#34; &gt;Erin Morgenstern&lt;/a&gt;.&lt;/p&gt;
&lt;p&gt;&lt;img src=&#34;https://wilw.dev/media/blog/night_circus.jpg&#34; alt=&#34;The Night Circus book cover&#34;&gt;&lt;/p&gt;
&lt;p&gt;The book is a sort of dark romantic/fantasy mashup. It’s about a travelling circus, those who perform in it, and those who run it.&lt;/p&gt;
&lt;p&gt;Le Cirque des Rêves is not just any circus, however. It’s only open at night, and always closes before dawn. What appears to be clever trickery may actually be much more behind the scenes, and the garish colours found in other circuses have been replaced by a simple black and white colour scheme.&lt;/p&gt;
&lt;p&gt;The story focuses on two young people - Celia and Marco - who are both participants in an unknown competition. As the story develops the characters discover more and more about themselves, each other, and the purpose and reason behind the circus itself.&lt;/p&gt;
&lt;p&gt;What I enjoyed most about the book is its characters. They are a mix of interesting people from different backgrounds, and each is refreshing in their own way. They are all intriguing, and usually likeable, despite some having perhaps darker and more selfish motives.&lt;/p&gt;
&lt;p&gt;The story itself also has interesting concepts. Although it jumps back and forth a bit between points and places in time and location, it’s great to observe how the characters and the various relationships between them grow. The “magic” and more technical elements are never really fully explained, which leaves significant parts of the story’s background to the reader to fill-in.&lt;/p&gt;
&lt;p&gt;I can recommend this book if you’re looking for an easy-going story and if you don’t mind finishing with a few unanswered questions on your mind!&lt;/p&gt;
</content:encoded>
    </item>
    
    <item>
      <title>Syncing RSS feeds with FreshRSS</title>
      <link>https://wilw.dev/blog/2021/07/12/freshrss/</link>
      <pubDate>Mon, 12 Jul 2021 11:00:00 +0000</pubDate>
      
      <guid>https://wilw.dev/blog/2021/07/12/freshrss/</guid>
      
        <category>100daystooffload</category>
      
        <category>technology</category>
      
        <category>selfhost</category>
      
      
      <content:encoded>&lt;p&gt;I enjoy reading my RSS feeds across my devices - whether it’s on my phone when out and about, my Mac in between bouts of work, or my iPad when in downtime.&lt;/p&gt;
&lt;p&gt;Being able to sync feeds across these devices is important to me, both so I can maintain a single collection of feeds and to ensure that I can keep track of read/unread articles as I switch devices.&lt;/p&gt;
&lt;p&gt;There are lots of web-based clients available, but using &lt;a href=&#34;https://reeder.app&#34; target=&#34;_blank&#34; rel=&#34;noopener noreferrer&#34; &gt;Reeder&lt;/a&gt; - a native app - gives a far nicer reading experience. There are lots of other clients for other types of devices too.&lt;/p&gt;
&lt;p&gt;By default, Reeder syncs using iCloud (I guess using &lt;a href=&#34;https://developer.apple.com/icloud/cloudkit&#34; target=&#34;_blank&#34; rel=&#34;noopener noreferrer&#34; &gt;CloudKit&lt;/a&gt;?). This means that the information is not easy to extract and is potentially subject to lock-in.&lt;/p&gt;
&lt;p&gt;Reeder also supports &lt;a href=&#34;https://freshrss.org&#34; target=&#34;_blank&#34; rel=&#34;noopener noreferrer&#34; &gt;FreshRSS&lt;/a&gt; as a backend. FreshRSS is a self-hosted feed aggregator with a web UI. Importantly, the project also includes a Google Reader compatible API, which allows it to act as a backend for Reeder (along with many other compatible clients).&lt;/p&gt;
&lt;p&gt;This reduces the lock-in, and there is a clear flow of data and source of truth. A much more open arrangement than syncing via iCloud.&lt;/p&gt;
&lt;p&gt;FreshRSS can easily be run &lt;a href=&#34;https://hub.docker.com/r/freshrss/freshrss&#34; target=&#34;_blank&#34; rel=&#34;noopener noreferrer&#34; &gt;using Docker&lt;/a&gt;. I recommend setting up a (sub-)domain for it and running it behind a reverse proxy that will handle the TLS side of things.&lt;/p&gt;
&lt;p&gt;Once setup, you should be able to navigate to your instance using the domain you chose in order to register a new account. Once logged-in you can begin to subscribe to feeds, import settings, and change some of the administrative options (e.g. to disallow additional registrations).&lt;/p&gt;
&lt;p&gt;One setting in particular (under &lt;code&gt;Administration&lt;/code&gt; -&gt; &lt;code&gt;Authentication&lt;/code&gt;) allows you to enable API access for use by app clients.&lt;/p&gt;
&lt;p&gt;&lt;img src=&#34;https://wilw.dev/media/blog/freshrss1.png&#34; alt=&#34;The authentication settings in FreshRSS&#34;&gt;&lt;/p&gt;
&lt;p&gt;Once that’s enabled you should be able to configure your client to use your FreshRSS instance as a backend. For example, in Reeder I go to &lt;code&gt;Accounts&lt;/code&gt; -&gt; &lt;code&gt;Add Account&lt;/code&gt; -&gt; &lt;code&gt;FreshRSS&lt;/code&gt; and then enter the server details:&lt;/p&gt;
&lt;p&gt;&lt;img src=&#34;https://wilw.dev/media/blog/freshrss2.png&#34; alt=&#34;Adding a new FreshRSS server in Reeder&#34;&gt;&lt;/p&gt;
&lt;p&gt;For the &lt;code&gt;Server&lt;/code&gt; field, enter your domain plus the &lt;code&gt;/api/greader.php&lt;/code&gt; path at the end to allow the client to integrate with the API.&lt;/p&gt;
&lt;p&gt;Once done, you can manage your feeds as usual by subscribing and organising into folders from your client. Then, if you login to your FreshRSS instance within clients on your other devices, everything should be kept in-sync.&lt;/p&gt;
</content:encoded>
    </item>
    
    <item>
      <title>The &#39;5AM Club&#39;</title>
      <link>https://wilw.dev/blog/2021/07/07/5am-club/</link>
      <pubDate>Wed, 07 Jul 2021 19:53:00 +0000</pubDate>
      
      <guid>https://wilw.dev/blog/2021/07/07/5am-club/</guid>
      
        <category>100daystooffload</category>
      
        <category>life</category>
      
      
      <content:encoded>&lt;p&gt;Recently my colleague was talking to me about the concept of the “5AM Club”, as defined in the &lt;a href=&#34;https://www.robinsharma.com/book/the-5am-club&#34; target=&#34;_blank&#34; rel=&#34;noopener noreferrer&#34; &gt;book by Robin Sharma&lt;/a&gt;.&lt;/p&gt;
&lt;p&gt;The “Club” is focused around starting your day early, with defined time slots for exercising and thinking.&lt;/p&gt;
&lt;p&gt;There’s a &lt;a href=&#34;https://www.youtube.com/watch?v=fGAUDm4PyEo&#34; target=&#34;_blank&#34; rel=&#34;noopener noreferrer&#34; &gt;great video here&lt;/a&gt; that summarises it all in about eight minutes.&lt;/p&gt;
&lt;p&gt;The rough idea is to get up strictly at 5AM, and then spend 20 minutes &lt;em&gt;exercising&lt;/em&gt;, 20 minutes &lt;em&gt;reflecting&lt;/em&gt;, and then a final 20 minutes &lt;em&gt;growing&lt;/em&gt;. By 6AM you are then energised and invigorated to start your day more effectively and successfully.&lt;/p&gt;
&lt;p&gt;The motivators for this are that many successful people - including Tim Cook and Oprah Winfrey - &lt;a href=&#34;https://www.cnbc.com/2018/05/17/10-highly-successful-people-who-wake-up-before-6-a-m.html&#34; target=&#34;_blank&#34; rel=&#34;noopener noreferrer&#34; &gt;begin their days early&lt;/a&gt;, and the concept of “if you want uncommon results [e.g. success] you need uncommon habits”.&lt;/p&gt;
&lt;p&gt;Although I find it difficult, I am a big believer in getting up early. I find that the days I &lt;a href=&#34;https://wilw.dev/blog/2021/05/12/running&#34;&gt;get up for a run at 6AM&lt;/a&gt; are the ones in which I do my best work, and I have a continual buzz that lasts all day.&lt;/p&gt;
&lt;p&gt;Conversely, when I stay in bed until 8AM I feel a little sluggish, and that feeling can last until the evening. This must definitely have an effect on my work and life outlook.&lt;/p&gt;
&lt;p&gt;Having said that, the ability to get up at 5AM for such a routine every day - whilst great on paper - may not work for all people. For example, those with young children awake all hours of the night (where every minute of extra sleep counts), or those with a social life in the evenings beyond 10PM.&lt;/p&gt;
&lt;p&gt;To get up at 5AM, and still get enough sleep, it would mean going to bed at 9PM each evening. I usually go to bed quite a bit later than this, and whilst I can (mostly!) get up for early runs when I need to, I think this additional commitment may be too much for me at this point in my life. But maybe these are sacrifices I should try and make?&lt;/p&gt;
&lt;p&gt;Do you have any “uncommon” habits that you think make you more effective? If so, I’d love to hear what worked for you!&lt;/p&gt;
</content:encoded>
    </item>
    
    <item>
      <title>How to resize images client-side in your webapps</title>
      <link>https://wilw.dev/blog/2021/07/06/resizing-images/</link>
      <pubDate>Tue, 06 Jul 2021 21:08:00 +0000</pubDate>
      
      <guid>https://wilw.dev/blog/2021/07/06/resizing-images/</guid>
      
        <category>100daystooffload</category>
      
        <category>technology</category>
      
      
      <content:encoded>&lt;h2 id=&#34;the-problem&#34;&gt;The problem&lt;/h2&gt;
&lt;p&gt;Image processing and resizing is a common task in many types of applications. This is made even more important by modern phones that take photos several megabytes in size.&lt;/p&gt;
&lt;p&gt;For example, if you offer an application that allows people to choose an avatar image, you won’t want to render the full multi-MB size image each time it’s shown. This is extra data for your users to download each time (which costs them both time and money, and can give a poor sluggish experience) and also means you need to fork out more cash for the additional bandwidth usage. If you have lots of users, then this time/money saving can be amplified significantly.&lt;/p&gt;
&lt;p&gt;Lots of tools exist for server-side processing. However this may mean you need to invest in infrastructure to handle asynchronous or scheduled server-side processing, or additional compute capacity to process image uploads on-the-fly.&lt;/p&gt;
&lt;p&gt;I’m a big fan of letting the users’ clients do more of the work, since this helps to distribute the compute power required. Allowing your user clients to upload files directly to a service such as S3 is often quicker and means you don’t need to provision and pay for the bigger server capacity yourself to deal with centralised processing of image files.&lt;/p&gt;
&lt;p&gt;So, to return to the avatar example above, what if your web front-end can instead resize the image before it is uploaded? This means you don’t need to do the additional server-side processing, you store less data, and the process is a little more &lt;em&gt;predictable&lt;/em&gt;.&lt;/p&gt;
&lt;h2 id=&#34;client-side-javascript-resizing&#34;&gt;Client-side JavaScript resizing&lt;/h2&gt;
&lt;p&gt;Modern browsers - both on desktops and mobile devices - are more than capable of doing a bit of additional work. For image-resizing, as you might have guessed, this involves making use of the HTML &lt;code&gt;canvas&lt;/code&gt; element.&lt;/p&gt;
&lt;p&gt;Essentially, the process involves the following steps:&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;Read in the user-chosen file (e.g. from a file chooser) as a data URL.&lt;/li&gt;
&lt;li&gt;Load the image from the file.&lt;/li&gt;
&lt;li&gt;Determine the correct dimensions for the image.&lt;/li&gt;
&lt;li&gt;Draw the image to the canvas.&lt;/li&gt;
&lt;li&gt;Build a new file based on the canvas.&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;The code below should help illustrate this further.&lt;/p&gt;
&lt;div class=&#34;highlight&#34;&gt;&lt;pre tabindex=&#34;0&#34; style=&#34;background-color:#f0f3f3;-moz-tab-size:2;-o-tab-size:2;tab-size:2;&#34;&gt;&lt;code class=&#34;language-javascript&#34; data-lang=&#34;javascript&#34;&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;&lt;span style=&#34;color:#09f;font-style:italic&#34;&gt;// Allow the caller to specify the file and a max width/height for the file
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;&lt;span style=&#34;color:#09f;font-style:italic&#34;&gt;&lt;/span&gt;&lt;span style=&#34;color:#069;font-weight:bold&#34;&gt;const&lt;/span&gt; resizeImage &lt;span style=&#34;color:#555&#34;&gt;=&lt;/span&gt; (file, maxWidth, maxHeight) =&gt; {
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;  &lt;span style=&#34;color:#069;font-weight:bold&#34;&gt;return&lt;/span&gt; &lt;span style=&#34;color:#069;font-weight:bold&#34;&gt;new&lt;/span&gt; &lt;span style=&#34;color:#366&#34;&gt;Promise&lt;/span&gt;((resolve, reject) =&gt; {
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;    &lt;span style=&#34;color:#09f;font-style:italic&#34;&gt;// Create a new FileReader
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;&lt;span style=&#34;color:#09f;font-style:italic&#34;&gt;&lt;/span&gt;    &lt;span style=&#34;color:#069;font-weight:bold&#34;&gt;const&lt;/span&gt; reader &lt;span style=&#34;color:#555&#34;&gt;=&lt;/span&gt; &lt;span style=&#34;color:#069;font-weight:bold&#34;&gt;new&lt;/span&gt; FileReader();
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;    &lt;span style=&#34;color:#09f;font-style:italic&#34;&gt;// Once the FileReader is ready...
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;&lt;span style=&#34;color:#09f;font-style:italic&#34;&gt;&lt;/span&gt;    reader.onload &lt;span style=&#34;color:#555&#34;&gt;=&lt;/span&gt; e =&gt; {
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;      &lt;span style=&#34;color:#09f;font-style:italic&#34;&gt;// Create a new image
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;&lt;span style=&#34;color:#09f;font-style:italic&#34;&gt;&lt;/span&gt;      &lt;span style=&#34;color:#069;font-weight:bold&#34;&gt;const&lt;/span&gt; img &lt;span style=&#34;color:#555&#34;&gt;=&lt;/span&gt; &lt;span style=&#34;color:#069;font-weight:bold&#34;&gt;new&lt;/span&gt; Image();
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;      &lt;span style=&#34;color:#09f;font-style:italic&#34;&gt;// Once the image is ready...
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;&lt;span style=&#34;color:#09f;font-style:italic&#34;&gt;&lt;/span&gt;      img.onload &lt;span style=&#34;color:#555&#34;&gt;=&lt;/span&gt; () =&gt; {
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;        &lt;span style=&#34;color:#09f;font-style:italic&#34;&gt;// Create a new canvas
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;&lt;span style=&#34;color:#09f;font-style:italic&#34;&gt;&lt;/span&gt;        &lt;span style=&#34;color:#069;font-weight:bold&#34;&gt;const&lt;/span&gt; canvas &lt;span style=&#34;color:#555&#34;&gt;=&lt;/span&gt; &lt;span style=&#34;color:#366&#34;&gt;document&lt;/span&gt;.createElement(&lt;span style=&#34;color:#c30&#34;&gt;&#39;canvas&#39;&lt;/span&gt;);
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;        &lt;span style=&#34;color:#069;font-weight:bold&#34;&gt;const&lt;/span&gt; ctx &lt;span style=&#34;color:#555&#34;&gt;=&lt;/span&gt; canvas.getContext(&lt;span style=&#34;color:#c30&#34;&gt;&#39;2d&#39;&lt;/span&gt;);
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;        &lt;span style=&#34;color:#09f;font-style:italic&#34;&gt;// Determine the new image dimensions based on maxWidth and maxHeight
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;&lt;span style=&#34;color:#09f;font-style:italic&#34;&gt;&lt;/span&gt;        &lt;span style=&#34;color:#069;font-weight:bold&#34;&gt;let&lt;/span&gt; width &lt;span style=&#34;color:#555&#34;&gt;=&lt;/span&gt; img.width;
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;        &lt;span style=&#34;color:#069;font-weight:bold&#34;&gt;let&lt;/span&gt; height &lt;span style=&#34;color:#555&#34;&gt;=&lt;/span&gt; img.height;
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;        &lt;span style=&#34;color:#069;font-weight:bold&#34;&gt;if&lt;/span&gt; (width &lt;span style=&#34;color:#555&#34;&gt;&gt;&lt;/span&gt; height) {
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;          &lt;span style=&#34;color:#069;font-weight:bold&#34;&gt;if&lt;/span&gt; (width &lt;span style=&#34;color:#555&#34;&gt;&gt;&lt;/span&gt; maxWidth) {
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;            height &lt;span style=&#34;color:#555&#34;&gt;*=&lt;/span&gt; maxWidth &lt;span style=&#34;color:#555&#34;&gt;/&lt;/span&gt; width;
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;            width &lt;span style=&#34;color:#555&#34;&gt;=&lt;/span&gt; maxWidth;
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;          }
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;        } &lt;span style=&#34;color:#069;font-weight:bold&#34;&gt;else&lt;/span&gt; {
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;          &lt;span style=&#34;color:#069;font-weight:bold&#34;&gt;if&lt;/span&gt; (height &lt;span style=&#34;color:#555&#34;&gt;&gt;&lt;/span&gt; maxHeight) {
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;            width &lt;span style=&#34;color:#555&#34;&gt;*=&lt;/span&gt; maxHeight &lt;span style=&#34;color:#555&#34;&gt;/&lt;/span&gt; height;
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;            height &lt;span style=&#34;color:#555&#34;&gt;=&lt;/span&gt; maxHeight;
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;          }
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;        }
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;        canvas.width &lt;span style=&#34;color:#555&#34;&gt;=&lt;/span&gt; width;
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;        canvas.height &lt;span style=&#34;color:#555&#34;&gt;=&lt;/span&gt; height;
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;        &lt;span style=&#34;color:#09f;font-style:italic&#34;&gt;// Draw the image to the canvas with the new sizes
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;&lt;span style=&#34;color:#09f;font-style:italic&#34;&gt;&lt;/span&gt;        ctx.drawImage(img, &lt;span style=&#34;color:#f60&#34;&gt;0&lt;/span&gt;, &lt;span style=&#34;color:#f60&#34;&gt;0&lt;/span&gt;, width, height);
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;        &lt;span style=&#34;color:#09f;font-style:italic&#34;&gt;// Build and return the resized image as an image file
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;&lt;span style=&#34;color:#09f;font-style:italic&#34;&gt;&lt;/span&gt;        canvas.toBlob(blob =&gt; {
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;          resolve(&lt;span style=&#34;color:#069;font-weight:bold&#34;&gt;new&lt;/span&gt; File([blob], file.name));
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;        }, &lt;span style=&#34;color:#c30&#34;&gt;&#39;image/jpeg&#39;&lt;/span&gt;, &lt;span style=&#34;color:#f60&#34;&gt;1&lt;/span&gt;);
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;      }
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;      &lt;span style=&#34;color:#09f;font-style:italic&#34;&gt;// Begin to load the image
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;&lt;span style=&#34;color:#09f;font-style:italic&#34;&gt;&lt;/span&gt;      img.src &lt;span style=&#34;color:#555&#34;&gt;=&lt;/span&gt; e.target.result;
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;    }
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;    &lt;span style=&#34;color:#09f;font-style:italic&#34;&gt;// Begin to load the file to the FileReader
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;&lt;span style=&#34;color:#09f;font-style:italic&#34;&gt;&lt;/span&gt;    reader.readAsDataURL(file);
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;  });
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;}
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;&lt;span style=&#34;color:#09f;font-style:italic&#34;&gt;// Elsewhere in your code (e.g. after the user has selected an image file)
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;&lt;span style=&#34;color:#09f;font-style:italic&#34;&gt;&lt;/span&gt;&lt;span style=&#34;color:#069;font-weight:bold&#34;&gt;const&lt;/span&gt; resizedFile &lt;span style=&#34;color:#555&#34;&gt;=&lt;/span&gt; &lt;span style=&#34;color:#069;font-weight:bold&#34;&gt;await&lt;/span&gt; resizeImage(file, &lt;span style=&#34;color:#f60&#34;&gt;400&lt;/span&gt;, &lt;span style=&#34;color:#f60&#34;&gt;400&lt;/span&gt;);
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;&lt;span style=&#34;color:#09f;font-style:italic&#34;&gt;// Now you can upload the resized image, etc....
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;At this point you can upload the &lt;code&gt;resizedFile&lt;/code&gt; directly to a storage provider, such as Amazon S3. In case it helps, there is more information on the direct-to-S3 file upload process in &lt;a href=&#34;https://devcenter.heroku.com/articles/s3-upload-node&#34; target=&#34;_blank&#34; rel=&#34;noopener noreferrer&#34; &gt;this article&lt;/a&gt;.&lt;/p&gt;
&lt;p&gt;If you care less about storage cost and would prefer to store multiple image sizes anyway (in case you want to offer users the chance to view the full-sized avatar too), then you can upload both &lt;code&gt;file&lt;/code&gt; and &lt;code&gt;resizedFile&lt;/code&gt;. You can then choose to serve one or the other in different scenarios.&lt;/p&gt;
&lt;p&gt;Either way, the key thing is that you don’t need any extra image-processing on the server. I’m sure there are more efficient methods than this in practice, but if you’re looking for a simple, no-dependency approach then I hope this might help!&lt;/p&gt;
</content:encoded>
    </item>
    
    <item>
      <title>Project Hail Mary by Andy Weir</title>
      <link>https://wilw.dev/blog/2021/07/05/project-hail-mary/</link>
      <pubDate>Mon, 05 Jul 2021 19:45:00 +0000</pubDate>
      
      <guid>https://wilw.dev/blog/2021/07/05/project-hail-mary/</guid>
      
        <category>100daystooffload</category>
      
        <category>book</category>
      
      
      <content:encoded>&lt;p&gt;&lt;a href=&#34;https://www.goodreads.com/author/show/6540057.Andy_Weir&#34; target=&#34;_blank&#34; rel=&#34;noopener noreferrer&#34; &gt;Andy Weir&lt;/a&gt; has become renowned over the past decade for his science fiction novels. &lt;a href=&#34;https://www.goodreads.com/book/show/18007564-the-martian&#34; target=&#34;_blank&#34; rel=&#34;noopener noreferrer&#34; &gt;&lt;em&gt;The Martian&lt;/em&gt;&lt;/a&gt; (and its movie) was hugely enjoyable and successful. I wasn’t so keen on &lt;a href=&#34;https://www.goodreads.com/book/show/34928122-artemis&#34; target=&#34;_blank&#34; rel=&#34;noopener noreferrer&#34; &gt;&lt;em&gt;Artemis&lt;/em&gt;&lt;/a&gt;, but still did enjoy the excitement of the story.&lt;/p&gt;
&lt;p&gt;I thought his latest book - &lt;a href=&#34;https://www.goodreads.com/book/show/54493401-project-hail-mary&#34; target=&#34;_blank&#34; rel=&#34;noopener noreferrer&#34; &gt;&lt;em&gt;Project Hail Mary&lt;/em&gt;&lt;/a&gt; - was fantastic.&lt;/p&gt;
&lt;p&gt;&lt;img src=&#34;https://wilw.dev/media/blog/project_hail_mary.jpg&#34; alt=&#34;Project Hail Mary book cover&#34;&gt;&lt;/p&gt;
&lt;p&gt;The story opens with a lone astronaut waking up in a spaceship that he has no memory of. He doesn’t know where he is, &lt;em&gt;who&lt;/em&gt; he is, or how he got there. Although he works out that he is of pivotal importance to the survival of the human race, the story cleverly keeps you guessing about what might come next right to the end.&lt;/p&gt;
&lt;p&gt;Whilst perhaps the story is a little more far-fetched than his other novels - particularly &lt;em&gt;The Martian&lt;/em&gt; - it still sits very much within the realm of possibility when compared to most other science fiction stories. It’s set in the - more or less - present day, and, as always, everything is explainable by the author using maths and science.&lt;/p&gt;
&lt;p&gt;This helps to make everything still feel “real” and relatable. The characters are all great, and each with their own quirks. I enjoyed how the story switches back and forth between the past and present in order to explain current events as the story continues to unfold. You build a bond with the astronaut as you learn things about his own past and the present developing situation together as the story progresses.&lt;/p&gt;
&lt;p&gt;My only wish was for the story to be longer! I felt that the ending was perhaps a little rushed and wasn’t as satisfying as I’d hoped it would be.&lt;/p&gt;
&lt;p&gt;Interestingly I also recently read &lt;a href=&#34;https://www.goodreads.com/book/show/32109569-we-are-legion-we-are-bob&#34; target=&#34;_blank&#34; rel=&#34;noopener noreferrer&#34; &gt;&lt;em&gt;We Are Legion (We Are Bob)&lt;/em&gt;&lt;/a&gt; by &lt;a href=&#34;https://www.goodreads.com/author/show/12130438.Dennis_E_Taylor&#34; target=&#34;_blank&#34; rel=&#34;noopener noreferrer&#34; &gt;Dennis E. Taylor&lt;/a&gt;. Published about five years ago, it follows a vaguely similar storyline with the main character leaving Earth on a mission to save humanity. The Audible versions of both books are narrated by the same person - &lt;a href=&#34;https://en.wikipedia.org/wiki/Ray_Porter&#34; target=&#34;_blank&#34; rel=&#34;noopener noreferrer&#34; &gt;Ray Porter&lt;/a&gt; - who is fantastic at injecting even more energy and excitement into the stories.&lt;/p&gt;
&lt;p&gt;&lt;em&gt;Project Hail Mary&lt;/em&gt; and &lt;em&gt;We Are Legion&lt;/em&gt; are relatively short reads (or listens), and so I can definitely recommend them both if you’re in the market for new fiction books.&lt;/p&gt;
</content:encoded>
    </item>
    
    <item>
      <title>Using Blurhash to create placeholders for your images</title>
      <link>https://wilw.dev/blog/2021/06/30/blurhash/</link>
      <pubDate>Wed, 30 Jun 2021 21:14:00 +0000</pubDate>
      
      <guid>https://wilw.dev/blog/2021/06/30/blurhash/</guid>
      
        <category>100daystooffload</category>
      
        <category>technology</category>
      
      
      <content:encoded>&lt;h2 id=&#34;loading-indicators&#34;&gt;Loading indicators&lt;/h2&gt;
&lt;p&gt;In user-facing software, loading indicators are extremely important to let your users know that something is happening. This is the same no matter whether your software is a CLI program, a GUI application, a web app - or anything else.&lt;/p&gt;
&lt;p&gt;Without such indicators, users of your software may become frustrated, or assume the program has crashed, and try to close it or leave.&lt;/p&gt;
&lt;p&gt;Generally speaking, developers should try to keep long-running tasks to a minimum (or even offload them to a cron-job or an asynchronous server-side worker). However, in some cases this is not possible. For example, in a cloud-based file storage solution, in which uploads and downloads are a core and direct user-facing feature, the user must wait until a bunch of files finish uploading - though any post-processing can of course still occur in the background afterwards.&lt;/p&gt;
&lt;p&gt;A loading indicator can be as simple as a &lt;code&gt;Loading...&lt;/code&gt; string (appropriate for CLI apps) or perhaps a spinner (appropriate for mobile and web apps). However there are lots of other interesting approaches to this too.&lt;/p&gt;
&lt;p&gt;For graphical user interfaces - particularly on the web - using placeholder layouts (or “skeleton screens”) is a great way to give the user an idea of what to expect in terms of &lt;em&gt;layout&lt;/em&gt; before the actual data finishes loading and rendering.&lt;/p&gt;
&lt;p&gt;&lt;img src=&#34;https://wilw.dev/media/blog/blurhash1.png&#34; alt=&#34;Skeleton screen on LinkedIn&#34;&gt;&lt;/p&gt;
&lt;h2 id=&#34;loading-text-vs-images&#34;&gt;Loading text vs images&lt;/h2&gt;
&lt;p&gt;In a GUI or web app, text data is quick to load due to its smaller payload size. For example, when making a request to a webpage containing a mix of text and larger images, the text will load first and render and the user must then wait until the images finish downloading before they can be displayed.&lt;/p&gt;
&lt;p&gt;This may cause the page to shift about once the images do load and get rendered on-screen - this is particularly annoying if the user has already begun reading and lose their point in the document as elements get re-positioned.&lt;/p&gt;
&lt;p&gt;One method to avoid this is to show an image &lt;em&gt;preview&lt;/em&gt;, which gets rendered at the same time as the text. The preview can then be “filled in” with the real image once it loads.&lt;/p&gt;
&lt;p&gt;To get an image preview on-screen at the same time as the surrounding text and other components one can deliver a smaller version of the image alongside the rest of the data. For example, by generating and delivering an inline data URL of an image directly within the HTML returned from the server.&lt;/p&gt;
&lt;p&gt;Modern web browsers are remarkably efficient at rendering this type of thing. However, it does require that you do some (probably) server-side image processing in order to derive a compressed version of the image before returning it in your HTTP response.&lt;/p&gt;
&lt;h2 id=&#34;blurhash&#34;&gt;Blurhash&lt;/h2&gt;
&lt;p&gt;A perhaps nicer way of accomplishing this is to use &lt;a href=&#34;https://github.com/woltapp/blurhash&#34; target=&#34;_blank&#34; rel=&#34;noopener noreferrer&#34; &gt;Blurhash&lt;/a&gt;. This tool enables the derivation of a compact string representation of an image that can easily be stored alongside your other data - right in your database - and can easily be returned in API payloads.&lt;/p&gt;
&lt;p&gt;Essentially, the library takes a “hash” of the image, which results in a short string. This string can then be decoded into a smaller image that can be rendered.&lt;/p&gt;
&lt;p&gt;As an example, we can use this picture of my dog:&lt;/p&gt;
&lt;p&gt;&lt;img src=&#34;https://wilw.dev/media/blog/blurhash2.jpg&#34; alt=&#34;Picture of a dog&#34;&gt;&lt;/p&gt;
&lt;p&gt;Using Blurhash to encode this image, the following string is returned: &lt;code&gt;URH_SPDl_HxZItM|Iqt7EQxrIpNI9uj?jboM&lt;/code&gt;. When decoding this string back into an image we get something like the below:&lt;/p&gt;
&lt;p&gt;&lt;img src=&#34;https://wilw.dev/media/blog/blurhash3.png&#34; alt=&#34;Blurhash&amp;rsquo;d picture of a dog&#34;&gt;&lt;/p&gt;
&lt;p&gt;It is clear that the second image is an approximation of the first. Given that the hash string is so short, downloading and rendering this as a placeholder before the full image itself downloads is quick and easy. It gives a nice preview of the image to the user for the short time it takes for the full image to load.&lt;/p&gt;
&lt;p&gt;Blurhash offers additional ways to tweak the hashing algorithm (e.g. to make it more or less “detailed”), and has implementations for many different languages and frameworks - across desktop, mobile, and web.&lt;/p&gt;
&lt;p&gt;I recommend checking out the &lt;a href=&#34;https://github.com/woltapp/blurhash&#34; target=&#34;_blank&#34; rel=&#34;noopener noreferrer&#34; &gt;documentation&lt;/a&gt; for more information on this.&lt;/p&gt;
&lt;h2 id=&#34;using-blurhash-in-a-javascript-app&#34;&gt;Using Blurhash in a JavaScript app&lt;/h2&gt;
&lt;p&gt;If you are writing a webapp, this section may help you get off the ground with Blurhash.&lt;/p&gt;
&lt;p&gt;Use of this library involves two steps; deriving the string “hash”, and then rendering this string as an image placeholder. To get started add &lt;a href=&#34;https://www.npmjs.com/package/blurhash&#34; target=&#34;_blank&#34; rel=&#34;noopener noreferrer&#34; &gt;blurhash&lt;/a&gt; to your project (e.g. with &lt;code&gt;yarn add blurhash&lt;/code&gt;).&lt;/p&gt;
&lt;p&gt;First of all we’ll look at getting the Blurhash string from an image. For this, we can do the processing browser-side to save on server resources. Assuming the user has just selected an image file for upload, the approach relies on drawing the specified image file to a hidden canvas that can be used to take the hash. The JavaScript code below illustrates this process.&lt;/p&gt;
&lt;div class=&#34;highlight&#34;&gt;&lt;pre tabindex=&#34;0&#34; style=&#34;background-color:#f0f3f3;-moz-tab-size:2;-o-tab-size:2;tab-size:2;&#34;&gt;&lt;code class=&#34;language-javascript&#34; data-lang=&#34;javascript&#34;&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;&lt;span style=&#34;color:#069;font-weight:bold&#34;&gt;import&lt;/span&gt; { encode } from &lt;span style=&#34;color:#c30&#34;&gt;&#39;blurhash&#39;&lt;/span&gt;;
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;&lt;span style=&#34;color:#069;font-weight:bold&#34;&gt;const&lt;/span&gt; getHash &lt;span style=&#34;color:#555&#34;&gt;=&lt;/span&gt; file =&gt; { 
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;  &lt;span style=&#34;color:#069;font-weight:bold&#34;&gt;const&lt;/span&gt; canvas &lt;span style=&#34;color:#555&#34;&gt;=&lt;/span&gt; &lt;span style=&#34;color:#366&#34;&gt;document&lt;/span&gt;.createElement(&lt;span style=&#34;color:#c30&#34;&gt;&#39;canvas&#39;&lt;/span&gt;); 
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;  &lt;span style=&#34;color:#069;font-weight:bold&#34;&gt;const&lt;/span&gt; context &lt;span style=&#34;color:#555&#34;&gt;=&lt;/span&gt; canvas.getContext(&lt;span style=&#34;color:#c30&#34;&gt;&#39;2d&#39;&lt;/span&gt;); 
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;  &lt;span style=&#34;color:#069;font-weight:bold&#34;&gt;const&lt;/span&gt; image &lt;span style=&#34;color:#555&#34;&gt;=&lt;/span&gt; &lt;span style=&#34;color:#069;font-weight:bold&#34;&gt;new&lt;/span&gt; Image(); 
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;  &lt;span style=&#34;color:#069;font-weight:bold&#34;&gt;return&lt;/span&gt; &lt;span style=&#34;color:#069;font-weight:bold&#34;&gt;new&lt;/span&gt; &lt;span style=&#34;color:#366&#34;&gt;Promise&lt;/span&gt;((resolve, reject) =&gt; { 
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;    image.onload &lt;span style=&#34;color:#555&#34;&gt;=&lt;/span&gt; () =&gt; { 
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;      canvas.width &lt;span style=&#34;color:#555&#34;&gt;=&lt;/span&gt; image.width; 
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;      canvas.height &lt;span style=&#34;color:#555&#34;&gt;=&lt;/span&gt; image.height; 
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;      context.drawImage(image, &lt;span style=&#34;color:#f60&#34;&gt;0&lt;/span&gt;, &lt;span style=&#34;color:#f60&#34;&gt;0&lt;/span&gt;); 
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;      &lt;span style=&#34;color:#069;font-weight:bold&#34;&gt;const&lt;/span&gt; imageData &lt;span style=&#34;color:#555&#34;&gt;=&lt;/span&gt; context.getImageData(&lt;span style=&#34;color:#f60&#34;&gt;0&lt;/span&gt;, &lt;span style=&#34;color:#f60&#34;&gt;0&lt;/span&gt;, image.width, image.height);
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;      resolve(encode(imageData.data, imageData.width, imageData.height, &lt;span style=&#34;color:#f60&#34;&gt;4&lt;/span&gt;, &lt;span style=&#34;color:#f60&#34;&gt;4&lt;/span&gt;)); 
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;    } 
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;    image.src &lt;span style=&#34;color:#555&#34;&gt;=&lt;/span&gt; URL.createObjectURL(file);
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;  }); 
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;}
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;&lt;span style=&#34;color:#09f;font-style:italic&#34;&gt;// Elsewhere in your code (e.g. once the user selects a file)
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;&lt;span style=&#34;color:#09f;font-style:italic&#34;&gt;&lt;/span&gt;&lt;span style=&#34;color:#069;font-weight:bold&#34;&gt;const&lt;/span&gt; hash &lt;span style=&#34;color:#555&#34;&gt;=&lt;/span&gt; &lt;span style=&#34;color:#069;font-weight:bold&#34;&gt;await&lt;/span&gt; getHash(file);
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;The &lt;code&gt;hash&lt;/code&gt; variable can then be sent up to your API and stored safely in your database.&lt;/p&gt;
&lt;p&gt;To later render the Blurhash string, it can be returned directly from your API along with other relevant information and then decoded into an image.&lt;/p&gt;
&lt;p&gt;Below is a React component - &lt;code&gt;BlurrableImage&lt;/code&gt;  - I use to render an image’s Blurhash whilst it loads in the background. Once loaded, the image gets rendered in place of the Blurhash.&lt;/p&gt;
&lt;div class=&#34;highlight&#34;&gt;&lt;pre tabindex=&#34;0&#34; style=&#34;background-color:#f0f3f3;-moz-tab-size:2;-o-tab-size:2;tab-size:2;&#34;&gt;&lt;code class=&#34;language-javascript&#34; data-lang=&#34;javascript&#34;&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;&lt;span style=&#34;color:#069;font-weight:bold&#34;&gt;import&lt;/span&gt; React, { useState, useEffect, useRef } from &lt;span style=&#34;color:#c30&#34;&gt;&#39;react&#39;&lt;/span&gt;; 
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;&lt;span style=&#34;color:#069;font-weight:bold&#34;&gt;import&lt;/span&gt; { decode } from &lt;span style=&#34;color:#c30&#34;&gt;&#39;blurhash&#39;&lt;/span&gt;; 
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;&lt;span style=&#34;color:#069;font-weight:bold&#34;&gt;function&lt;/span&gt; BlurrableImage({ src, blurHash }) { 
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;  &lt;span style=&#34;color:#069;font-weight:bold&#34;&gt;const&lt;/span&gt; [loaded, setLoaded] &lt;span style=&#34;color:#555&#34;&gt;=&lt;/span&gt; useState(&lt;span style=&#34;color:#069;font-weight:bold&#34;&gt;false&lt;/span&gt;); 
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;  &lt;span style=&#34;color:#069;font-weight:bold&#34;&gt;const&lt;/span&gt; canvasRef &lt;span style=&#34;color:#555&#34;&gt;=&lt;/span&gt; useRef();  
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;  useEffect(() =&gt; { 
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;    &lt;span style=&#34;color:#069;font-weight:bold&#34;&gt;const&lt;/span&gt; canvas &lt;span style=&#34;color:#555&#34;&gt;=&lt;/span&gt; canvasRef.current; 
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;    &lt;span style=&#34;color:#069;font-weight:bold&#34;&gt;const&lt;/span&gt; context &lt;span style=&#34;color:#555&#34;&gt;=&lt;/span&gt; canvas.getContext(&lt;span style=&#34;color:#c30&#34;&gt;&#39;2d&#39;&lt;/span&gt;); 
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;    &lt;span style=&#34;color:#069;font-weight:bold&#34;&gt;const&lt;/span&gt; imageData &lt;span style=&#34;color:#555&#34;&gt;=&lt;/span&gt; context.createImageData(&lt;span style=&#34;color:#f60&#34;&gt;200&lt;/span&gt;, &lt;span style=&#34;color:#f60&#34;&gt;200&lt;/span&gt;); 
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;    &lt;span style=&#34;color:#069;font-weight:bold&#34;&gt;const&lt;/span&gt; decodedHash &lt;span style=&#34;color:#555&#34;&gt;=&lt;/span&gt; decode(blurHash, &lt;span style=&#34;color:#f60&#34;&gt;200&lt;/span&gt;, &lt;span style=&#34;color:#f60&#34;&gt;200&lt;/span&gt;); 
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;    imageData.data.set(decodedHash); 
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;    context.putImageData(imageData, &lt;span style=&#34;color:#f60&#34;&gt;0&lt;/span&gt;, &lt;span style=&#34;color:#f60&#34;&gt;0&lt;/span&gt;); 
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;  }, [blurHash]); 
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;  &lt;span style=&#34;color:#069;font-weight:bold&#34;&gt;return&lt;/span&gt; (&lt;span style=&#34;color:#555&#34;&gt;&lt;&gt;&lt;/span&gt; 
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;    {&lt;span style=&#34;color:#09f;font-style:italic&#34;&gt;/* This image will never display. We just use it so we know when the browser has downloaded it. */&lt;/span&gt;}
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;    &lt;span style=&#34;color:#555&#34;&gt;&lt;&lt;/span&gt;img 
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;      src&lt;span style=&#34;color:#555&#34;&gt;=&lt;/span&gt;{src} 
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;      onLoad&lt;span style=&#34;color:#555&#34;&gt;=&lt;/span&gt;{e =&gt; setLoaded(&lt;span style=&#34;color:#069;font-weight:bold&#34;&gt;true&lt;/span&gt;)}
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;      style&lt;span style=&#34;color:#555&#34;&gt;=&lt;/span&gt;{{ display&lt;span style=&#34;color:#555&#34;&gt;:&lt;/span&gt; &lt;span style=&#34;color:#c30&#34;&gt;&#39;none&#39;&lt;/span&gt; }} 
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;    &lt;span style=&#34;color:#555&#34;&gt;/&gt;&lt;/span&gt; 
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;    {&lt;span style=&#34;color:#09f;font-style:italic&#34;&gt;/* When the image has been downloaded, we can render it. E.g. here we use it as a background image. */&lt;/span&gt;}
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;    {loaded &lt;span style=&#34;color:#555&#34;&gt;&amp;&amp;&lt;/span&gt;  
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;      &lt;span style=&#34;color:#555&#34;&gt;&lt;&lt;/span&gt;div style&lt;span style=&#34;color:#555&#34;&gt;=&lt;/span&gt;{{
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;        width&lt;span style=&#34;color:#555&#34;&gt;:&lt;/span&gt; &lt;span style=&#34;color:#f60&#34;&gt;200&lt;/span&gt;, height&lt;span style=&#34;color:#555&#34;&gt;:&lt;/span&gt; &lt;span style=&#34;color:#f60&#34;&gt;200&lt;/span&gt;, 
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;        backgoundSize&lt;span style=&#34;color:#555&#34;&gt;:&lt;/span&gt; &lt;span style=&#34;color:#c30&#34;&gt;&#39;cover&#39;&lt;/span&gt;, 
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;        backgroundPosition&lt;span style=&#34;color:#555&#34;&gt;:&lt;/span&gt; &lt;span style=&#34;color:#c30&#34;&gt;&#39;center&#39;&lt;/span&gt;,
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;        backgroundImage&lt;span style=&#34;color:#555&#34;&gt;:&lt;/span&gt; &lt;span style=&#34;color:#c30&#34;&gt;`url(&lt;/span&gt;&lt;span style=&#34;color:#a00&#34;&gt;${&lt;/span&gt;src&lt;span style=&#34;color:#a00&#34;&gt;}&lt;/span&gt;&lt;span style=&#34;color:#c30&#34;&gt;)`&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;      }} &lt;span style=&#34;color:#555&#34;&gt;/&gt;&lt;/span&gt; 
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;    } 
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;    {&lt;span style=&#34;color:#09f;font-style:italic&#34;&gt;/* We only show this canvas while loaded == false */&lt;/span&gt;}
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;    &lt;span style=&#34;color:#555&#34;&gt;&lt;&lt;/span&gt;canvas 
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;      width&lt;span style=&#34;color:#555&#34;&gt;=&lt;/span&gt;{&lt;span style=&#34;color:#f60&#34;&gt;200&lt;/span&gt;} height&lt;span style=&#34;color:#555&#34;&gt;=&lt;/span&gt;{&lt;span style=&#34;color:#f60&#34;&gt;200&lt;/span&gt;} 
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;      ref&lt;span style=&#34;color:#555&#34;&gt;=&lt;/span&gt;{canvasRef} 
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;      style&lt;span style=&#34;color:#555&#34;&gt;=&lt;/span&gt;{{ display&lt;span style=&#34;color:#555&#34;&gt;:&lt;/span&gt; &lt;span style=&#34;color:#555&#34;&gt;!&lt;/span&gt;loaded &lt;span style=&#34;color:#555&#34;&gt;?&lt;/span&gt; &lt;span style=&#34;color:#c30&#34;&gt;&#39;block&#39;&lt;/span&gt; &lt;span style=&#34;color:#555&#34;&gt;:&lt;/span&gt; &lt;span style=&#34;color:#c30&#34;&gt;&#39;none&#39;&lt;/span&gt; }}
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;    &lt;span style=&#34;color:#555&#34;&gt;/&gt;&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;  &lt;span style=&#34;color:#555&#34;&gt;&lt;&lt;/span&gt;&lt;span style=&#34;color:#a00;background-color:#faa&#34;&gt;/&gt;); &lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;} 
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;&lt;span style=&#34;color:#069;font-weight:bold&#34;&gt;export&lt;/span&gt; &lt;span style=&#34;color:#069;font-weight:bold&#34;&gt;default&lt;/span&gt; BlurrableImage; 
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;If you display images in your application, Blurhash might offer a good solution for keeping your interfaces speedy and intuitive.&lt;/p&gt;
</content:encoded>
    </item>
    
    <item>
      <title>Self-host your web searches with Whoogle</title>
      <link>https://wilw.dev/blog/2021/06/24/whoogle/</link>
      <pubDate>Thu, 24 Jun 2021 21:48:00 +0000</pubDate>
      
      <guid>https://wilw.dev/blog/2021/06/24/whoogle/</guid>
      
        <category>100daystooffload</category>
      
        <category>technology</category>
      
        <category>selfhost</category>
      
      
      <content:encoded>&lt;h2 id=&#34;google-and-duckduckgo&#34;&gt;Google and DuckDuckGo&lt;/h2&gt;
&lt;p&gt;It’s common knowledge that part of Google’s business model is to use the data it knows about you, your searches, and browsing patterns in order to more effectively serve ads.&lt;/p&gt;
&lt;p&gt;Many people feel uncomfortable with this and so there is a strong movement to adopt more privacy-focused options, such as &lt;a href=&#34;https://duckduckgo.com&#34; target=&#34;_blank&#34; rel=&#34;noopener noreferrer&#34; &gt;DuckDuckGo&lt;/a&gt;. This was my position, too. For a few years I’ve been a solid DuckDuckGo user, and it was my default on Mac and mobile devices.&lt;/p&gt;
&lt;p&gt;However, I do find that for more technical queries - e.g. for specific parts of an API’s documentation - it doesn’t perform as well as Google. DuckDuckGo supports using &lt;a href=&#34;https://duckduckgo.com/bang&#34; target=&#34;_blank&#34; rel=&#34;noopener noreferrer&#34; &gt;bangs&lt;/a&gt; for automatically forwarding searches to another service. For example, prepending a search with &lt;code&gt;!g&lt;/code&gt; will forward the query to Google instead.&lt;/p&gt;
&lt;p&gt;As time went by, I found myself using &lt;code&gt;!g&lt;/code&gt; more and more - for both technical and non-technical searches. It got to the point where I was just &lt;code&gt;!g&lt;/code&gt;-ing everything. And so I wondered what the point was in proxying through DuckDuckGo at all.&lt;/p&gt;
&lt;h2 id=&#34;self-hosting-whoogle&#34;&gt;Self-hosting Whoogle&lt;/h2&gt;
&lt;p&gt;Not long ago I saw a link to &lt;a href=&#34;https://github.com/benbusby/whoogle-search&#34; target=&#34;_blank&#34; rel=&#34;noopener noreferrer&#34; &gt;Whoogle Search&lt;/a&gt; - a self-hosted open-source Google search replacement.&lt;/p&gt;
&lt;p&gt;Whoogle does not display ads, doesn’t rely on lots of JavaScript, and still returns great results. As long as it’s hosted somewhere in the cloud then there is no reason for Google to be able to track you, either.&lt;/p&gt;
&lt;p&gt;Getting it up and running on one of my servers was super easy. I use Docker to deploy services and so after pointing a subdomain to the server, setting up the needed certificates, and adding a virtual host to my &lt;code&gt;nginx&lt;/code&gt; container, all I needed to do to get Whoogle running was to pull and run the container:&lt;/p&gt;
&lt;div class=&#34;highlight&#34;&gt;&lt;pre tabindex=&#34;0&#34; style=&#34;background-color:#f0f3f3;-moz-tab-size:2;-o-tab-size:2;tab-size:2;&#34;&gt;&lt;code class=&#34;language-shell&#34; data-lang=&#34;shell&#34;&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;docker run -d -p 5000:5000 benbusby/whoogle-search:latest
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;I could then browse to the server and begin searching.&lt;/p&gt;
&lt;p&gt;There are lots of ways the deployment can be tweaked, including the interface and security settings, so be sure to take a look at the documentation for &lt;a href=&#34;https://github.com/benbusby/whoogle-search&#34; target=&#34;_blank&#34; rel=&#34;noopener noreferrer&#34; &gt;more options&lt;/a&gt;.&lt;/p&gt;
&lt;h2 id=&#34;setting-whoogle-as-firefoxs-default-search-engine&#34;&gt;Setting Whoogle as Firefox’s default search engine&lt;/h2&gt;
&lt;p&gt;The main way I search the web is to simply type my query into the address bar in Firefox. To make the best use of Whoogle I needed to configure Firefox to use Whoogle as my default search provider.&lt;/p&gt;
&lt;p&gt;There is no direct option for this in the Firefox settings, however Whoogle complies with the &lt;a href=&#34;https://en.wikipedia.org/wiki/OpenSearch&#34; target=&#34;_blank&#34; rel=&#34;noopener noreferrer&#34; &gt;OpenSearch standard&lt;/a&gt;. This means that if you visit your self-hosted search-engine in Firefox you should be able to add it to Firefox by right-clicking the address bar and selecting &lt;em&gt;Add “Whoogle Search”&lt;/em&gt;.&lt;/p&gt;
&lt;p&gt;&lt;img src=&#34;https://wilw.dev/media/blog/whoogle1.png&#34; alt=&#34;Adding Whoogle as a search engine&#34;&gt;&lt;/p&gt;
&lt;p&gt;Once Whoogle is added to Firefox it can be set as the default search engine through the standard Firefox settings.&lt;/p&gt;
&lt;p&gt;On mobile Firefox - which is even more great now that iOS allows for selecting a default browser! - the process is slightly different. Simply tap &lt;em&gt;Add search engine&lt;/em&gt; in the app’s settings, and in the URL box enter &lt;code&gt;https://server.address/search?q=%s&lt;/code&gt; (replacing with your server’s address) to add your Whoogle server as your search engine.&lt;/p&gt;
&lt;p&gt;If you’re someone that enjoys self-hosting things, then I recommend giving Whoogle a try.&lt;/p&gt;
</content:encoded>
    </item>
    
    <item>
      <title>Joining a panel at Wales Tech Week</title>
      <link>https://wilw.dev/blog/2021/06/23/wales-tech-week/</link>
      <pubDate>Wed, 23 Jun 2021 22:21:00 +0000</pubDate>
      
      <guid>https://wilw.dev/blog/2021/06/23/wales-tech-week/</guid>
      
        <category>100daystooffload</category>
      
        <category>technology</category>
      
        <category>life</category>
      
      
      <content:encoded>&lt;p&gt;&lt;a href=&#34;https://www.canva.com/design/DAEe1QqwIPw/SJx9TDgReq4B_V5Vc2w9sA/view?website&#34; target=&#34;_blank&#34; rel=&#34;noopener noreferrer&#34; &gt;Wales Tech Week&lt;/a&gt; is an annual event held by &lt;a href=&#34;https://technologyconnected.net&#34; target=&#34;_blank&#34; rel=&#34;noopener noreferrer&#34; &gt;Technology Connected&lt;/a&gt;. The 2021 event is running this week, aiming to bring technologists together from a wide range of businesses and organisations across Wales.&lt;/p&gt;
&lt;p&gt;Today, I was a member of a panel discussing blockchain - &lt;em&gt;“Welsh Businesses Bringing Blockchain to Life”&lt;/em&gt;. I was speaking alongside experts from other companies working in the blockchain and crypto space, and an academic focused on applying the technology to government functions.&lt;/p&gt;
&lt;p&gt;&lt;img src=&#34;https://wilw.dev/media/blog/wales_tech_week.jpg&#34; alt=&#34;Screenshot from the event&#34;&gt;&lt;/p&gt;
&lt;p&gt;It was a great opportunity to talk about how we are using blockchain at &lt;a href=&#34;https://www.simplydo.co.uk&#34; target=&#34;_blank&#34; rel=&#34;noopener noreferrer&#34; &gt;Simply Do&lt;/a&gt; to power some of our complex supply-chain processes, and to also hear about the exciting work from the other panellists.&lt;/p&gt;
&lt;p&gt;The panel was excellently chaired by David Blake from the &lt;a href=&#34;https://developmentbank.wales&#34; target=&#34;_blank&#34; rel=&#34;noopener noreferrer&#34; &gt;Development Bank of Wales&lt;/a&gt;. They are one of our investors and have been of great help and support to us over the past few years.&lt;/p&gt;
&lt;p&gt;I look forward to continue watching the events over the next couple of days.&lt;/p&gt;
</content:encoded>
    </item>
    
    <item>
      <title>Anxious People by Fredrik Backman</title>
      <link>https://wilw.dev/blog/2021/06/19/anxious-people/</link>
      <pubDate>Sat, 19 Jun 2021 18:13:00 +0000</pubDate>
      
      <guid>https://wilw.dev/blog/2021/06/19/anxious-people/</guid>
      
        <category>100daystooffload</category>
      
        <category>book</category>
      
      
      <content:encoded>&lt;p&gt;&lt;a href=&#34;https://www.goodreads.com/book/show/49127718-anxious-people&#34; target=&#34;_blank&#34; rel=&#34;noopener noreferrer&#34; &gt;&lt;em&gt;Anxious People&lt;/em&gt;&lt;/a&gt; is a book about an attempted bank robbery in a Swedish town (not Stockholm!). It is written by &lt;a href=&#34;https://www.goodreads.com/author/show/6485178.Fredrik_Backman&#34; target=&#34;_blank&#34; rel=&#34;noopener noreferrer&#34; &gt;Fredrik Backman&lt;/a&gt;.&lt;/p&gt;
&lt;p&gt;&lt;img src=&#34;https://wilw.dev/media/blog/anxious_people.jpg&#34; alt=&#34;Anxious People book cover&#34;&gt;&lt;/p&gt;
&lt;p&gt;The story involves a would-be bank robber arriving unexpectedly at an open apartment viewing whilst trying to run away, and taking the prospective buyers hostage in the process. It is mostly split between being set at the apartment itself and the police station in which the hostages are separately interviewed after the event. It is told primarily from the perspectives of the bank robber, the hostages, and the police officers.&lt;/p&gt;
&lt;p&gt;I think that the first few chapters set this book off in the wrong light - they seem a little childish and appear to be filled with annoying and unconvincing characters. However, once past the first few scene-setting parts the story comes into its own.&lt;/p&gt;
&lt;p&gt;Very quickly I got the impression of a deeply intertwined collection of lives that span across all of the characters, from the hostages to the bank robber and the police officers. The interconnection also spans across &lt;em&gt;time&lt;/em&gt;, with past events that affected one or more of the characters having impact on their present lives, and the particular situation at hand.&lt;/p&gt;
&lt;p&gt;The author cleverly introduces concepts and events earlier on in the novel, which then solidify and take on further meaning and importance as each character’s story progresses further.&lt;/p&gt;
&lt;p&gt;Characters I didn’t really like at the start of the novel soon become more relatable as I understood them more clearly towards the end. It’s definitely an interesting book and one I can recommend as a light, but thought-provoking, read.&lt;/p&gt;
</content:encoded>
    </item>
    
    <item>
      <title>Tmuxinator: simple terminal workspaces for your projects</title>
      <link>https://wilw.dev/blog/2021/06/18/tmuxinator/</link>
      <pubDate>Fri, 18 Jun 2021 19:55:00 +0000</pubDate>
      
      <guid>https://wilw.dev/blog/2021/06/18/tmuxinator/</guid>
      
        <category>100daystooffload</category>
      
        <category>technology</category>
      
      
      <content:encoded>&lt;h2 id=&#34;living-without-workspaces&#34;&gt;Living without workspaces&lt;/h2&gt;
&lt;p&gt;IDEs and richly-featured text editors - such as &lt;a href=&#34;https://code.visualstudio.com&#34; target=&#34;_blank&#34; rel=&#34;noopener noreferrer&#34; &gt;VS Code&lt;/a&gt; and &lt;a href=&#34;https://www.sublimetext.com&#34; target=&#34;_blank&#34; rel=&#34;noopener noreferrer&#34; &gt;Sublime Text&lt;/a&gt; - support many great features. One of these is the notion of &lt;em&gt;projects&lt;/em&gt; or &lt;em&gt;workspaces&lt;/em&gt;.&lt;/p&gt;
&lt;p&gt;Such workspaces let you save your project’s development configuration to disk - things like the project directory, open files, editor layout, integrated terminal commands, and more. Often, each project can have its own workspace, too.&lt;/p&gt;
&lt;p&gt;If you use workspaces then you don’t need to go through the tedious process of setting everything back up again each time you switch project, re-open your editor, or reboot your computer.&lt;/p&gt;
&lt;p&gt;However, if, like me, you use the terminal as a primary development environment, things don’t work quite so nicely out of the box. For example, I use &lt;a href=&#34;https://github.com/tmux/tmux/wiki&#34; target=&#34;_blank&#34; rel=&#34;noopener noreferrer&#34; &gt;tmux&lt;/a&gt; as my primary development environment, and make use of multiple windows and panes for things like Vim, source control, logs, and running commands.&lt;/p&gt;
&lt;p&gt;At any given time, I might have a handful of tmux sessions running (one for each project). A single small session project might consist of a web API service and a separate front-end - each comprising Vim editor panes, and a mix of other things. Context switching is super easy, as I can just detach from a session, and then re-attach to another one that tmux has kept running for me in the background.&lt;/p&gt;
&lt;p&gt;However, the pain point comes when rebooting. Once the tmux server process terminates, all of the running sessions are lost. This means setting each session up again individually each time you want to begin working on a different project after rebooting.&lt;/p&gt;
&lt;p&gt;It certainly feels like a blocker to performing system upgrades that require reboots, and is also extra friction that may prevent one from working on specific projects if the set-up is too painstaking. Both of these are clearly not good.&lt;/p&gt;
&lt;p&gt;However, there is a solution: &lt;a href=&#34;https://github.com/tmuxinator/tmuxinator&#34; target=&#34;_blank&#34; rel=&#34;noopener noreferrer&#34; &gt;tmuxinator&lt;/a&gt;.&lt;/p&gt;
&lt;h2 id=&#34;tmuxinator&#34;&gt;Tmuxinator&lt;/h2&gt;
&lt;p&gt;Tmuxinator is a program that partly aims to try and fix the workspace problem for tmux-based workflows, and my life is so much easier because of it.&lt;/p&gt;
&lt;p&gt;The program does not interfere with the tmux server directly, and neither does it maintain individual explicit tmux session data - tmux sessions are still lost after reboot.&lt;/p&gt;
&lt;p&gt;However, what it &lt;em&gt;does&lt;/em&gt; do is make workspace session management so much easier by storing your project window and pane layout in a simple YAML file on disk.&lt;/p&gt;
&lt;p&gt;For example, a simple API and separate web front-end project (as mentioned above) could be described as the following tmuxinator project:&lt;/p&gt;
&lt;div class=&#34;highlight&#34;&gt;&lt;pre tabindex=&#34;0&#34; style=&#34;background-color:#f0f3f3;-moz-tab-size:2;-o-tab-size:2;tab-size:2;&#34;&gt;&lt;code class=&#34;language-yaml&#34; data-lang=&#34;yaml&#34;&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;&lt;span style=&#34;color:#309;font-weight:bold&#34;&gt;name&lt;/span&gt;:&lt;span style=&#34;color:#bbb&#34;&gt; &lt;/span&gt;cool-project&lt;span style=&#34;color:#bbb&#34;&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;&lt;span style=&#34;color:#bbb&#34;&gt;&lt;/span&gt;&lt;span style=&#34;color:#309;font-weight:bold&#34;&gt;root&lt;/span&gt;:&lt;span style=&#34;color:#bbb&#34;&gt; &lt;/span&gt;~/project/my-cool-web-project&lt;span style=&#34;color:#bbb&#34;&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;&lt;span style=&#34;color:#bbb&#34;&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;&lt;span style=&#34;color:#bbb&#34;&gt;&lt;/span&gt;&lt;span style=&#34;color:#309;font-weight:bold&#34;&gt;windows&lt;/span&gt;:&lt;span style=&#34;color:#bbb&#34;&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;&lt;span style=&#34;color:#bbb&#34;&gt;  &lt;/span&gt;- &lt;span style=&#34;color:#309;font-weight:bold&#34;&gt;api&lt;/span&gt;:&lt;span style=&#34;color:#bbb&#34;&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;&lt;span style=&#34;color:#bbb&#34;&gt;      &lt;/span&gt;&lt;span style=&#34;color:#309;font-weight:bold&#34;&gt;root&lt;/span&gt;:&lt;span style=&#34;color:#bbb&#34;&gt; &lt;/span&gt;~/project/my-cool-web-project/api&lt;span style=&#34;color:#bbb&#34;&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;&lt;span style=&#34;color:#bbb&#34;&gt;      &lt;/span&gt;&lt;span style=&#34;color:#309;font-weight:bold&#34;&gt;layout&lt;/span&gt;:&lt;span style=&#34;color:#bbb&#34;&gt; &lt;/span&gt;main-vertical&lt;span style=&#34;color:#bbb&#34;&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;&lt;span style=&#34;color:#bbb&#34;&gt;      &lt;/span&gt;&lt;span style=&#34;color:#309;font-weight:bold&#34;&gt;panes&lt;/span&gt;:&lt;span style=&#34;color:#bbb&#34;&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;&lt;span style=&#34;color:#bbb&#34;&gt;        &lt;/span&gt;- vim&lt;span style=&#34;color:#bbb&#34;&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;&lt;span style=&#34;color:#bbb&#34;&gt;        &lt;/span&gt;- &lt;span style=&#34;color:#309;font-weight:bold&#34;&gt;app&lt;/span&gt;:&lt;span style=&#34;color:#bbb&#34;&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;&lt;span style=&#34;color:#bbb&#34;&gt;          &lt;/span&gt;- source .venv/bin/activate&lt;span style=&#34;color:#bbb&#34;&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;&lt;span style=&#34;color:#bbb&#34;&gt;          &lt;/span&gt;- source .envfile&lt;span style=&#34;color:#bbb&#34;&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;&lt;span style=&#34;color:#bbb&#34;&gt;          &lt;/span&gt;- flask run&lt;span style=&#34;color:#bbb&#34;&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;&lt;span style=&#34;color:#bbb&#34;&gt;        &lt;/span&gt;- zsh&lt;span style=&#34;color:#bbb&#34;&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;&lt;span style=&#34;color:#bbb&#34;&gt;  &lt;/span&gt;- &lt;span style=&#34;color:#309;font-weight:bold&#34;&gt;frontend&lt;/span&gt;:&lt;span style=&#34;color:#bbb&#34;&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;&lt;span style=&#34;color:#bbb&#34;&gt;      &lt;/span&gt;&lt;span style=&#34;color:#309;font-weight:bold&#34;&gt;root&lt;/span&gt;:&lt;span style=&#34;color:#bbb&#34;&gt; &lt;/span&gt;~/project/my-cool-web-project/web&lt;span style=&#34;color:#bbb&#34;&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;&lt;span style=&#34;color:#bbb&#34;&gt;      &lt;/span&gt;&lt;span style=&#34;color:#309;font-weight:bold&#34;&gt;layout&lt;/span&gt;:&lt;span style=&#34;color:#bbb&#34;&gt; &lt;/span&gt;main-vertical&lt;span style=&#34;color:#bbb&#34;&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;&lt;span style=&#34;color:#bbb&#34;&gt;      &lt;/span&gt;&lt;span style=&#34;color:#309;font-weight:bold&#34;&gt;panes&lt;/span&gt;:&lt;span style=&#34;color:#bbb&#34;&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;&lt;span style=&#34;color:#bbb&#34;&gt;        &lt;/span&gt;- vim&lt;span style=&#34;color:#bbb&#34;&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;&lt;span style=&#34;color:#bbb&#34;&gt;        &lt;/span&gt;- yarn start&lt;span style=&#34;color:#bbb&#34;&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;&lt;span style=&#34;color:#bbb&#34;&gt;        &lt;/span&gt;- zsh&lt;span style=&#34;color:#bbb&#34;&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;This project represents two tmux windows, each with three panes: an editor, a server (or watcher), and an empty shell pane that can be used for issuing commands (like &lt;code&gt;git&lt;/code&gt;). The inbuilt &lt;code&gt;main-vertical&lt;/code&gt; layout automatically provides a nice, big Vim window (in these cases) for editing, and then a vertically-split extra pair of panes.&lt;/p&gt;
&lt;p&gt;Each window has a separate root directory, and the project as a whole has its own root directory too, to provide better automatic working directories in case new windows are later created when inside the session. Each session and window also gets its own name (e.g. &lt;code&gt;api&lt;/code&gt; and &lt;code&gt;frontend&lt;/code&gt; above) to make identification easier later on.&lt;/p&gt;
&lt;p&gt;If this file is stored in &lt;code&gt;~/.config/tmuxinator/cool-project.yml&lt;/code&gt;, one can simply run &lt;code&gt;tmuxinator start cool-project&lt;/code&gt; to get started. If the project is already running it will attach you to it as-is. If the project is not currently up and running, tmuxinator will go ahead and create all your windows and panes, run the commands you specify, and connect you to the new session.&lt;/p&gt;
&lt;p&gt;Once inside the session, it’s just controlled by plain old tmux. To detach from the session, just use the usual tmux sequence (by default &lt;code&gt;ctrl-B-D&lt;/code&gt;). You can then connect back to the same session or another one.&lt;/p&gt;
&lt;h2 id=&#34;creating-more-project-configurations&#34;&gt;Creating more project configurations&lt;/h2&gt;
&lt;p&gt;Tmuxinator comes with lots of other commands to make set-up easier. For example, running &lt;code&gt;tmuxinator new &lt;project name&gt;&lt;/code&gt; will open up your editor on a template YAML file for you to edit and then save.&lt;/p&gt;
&lt;p&gt;If you have lots of similar types of projects with the same layouts then the copy command is useful for duplicating projects as a convenient start-point: &lt;code&gt;tmuxinator cp &lt;existing&gt; &lt;new&gt;&lt;/code&gt;. You can also list and delete projects in a similar way: &lt;code&gt;tmuxinator ls&lt;/code&gt; and &lt;code&gt;tmuxinator rm &lt;project name&gt;&lt;/code&gt;.&lt;/p&gt;
&lt;p&gt;Definitely take a look through the &lt;a href=&#34;https://github.com/tmuxinator/tmuxinator&#34; target=&#34;_blank&#34; rel=&#34;noopener noreferrer&#34; &gt;documentation&lt;/a&gt; to learn more about these and other commands.&lt;/p&gt;
&lt;h2 id=&#34;installing-tmuxinator&#34;&gt;Installing tmuxinator&lt;/h2&gt;
&lt;p&gt;Many distributions include tmuxinator in their repos. On macOS it’s a simple &lt;code&gt;brew install tmuxinator&lt;/code&gt;.&lt;/p&gt;
&lt;p&gt;Take a look at the &lt;a href=&#34;https://github.com/tmuxinator/tmuxinator#installation&#34; target=&#34;_blank&#34; rel=&#34;noopener noreferrer&#34; &gt;installation instructions&lt;/a&gt; for more information.&lt;/p&gt;
&lt;h2 id=&#34;backing-up-and-syncing-projects&#34;&gt;Backing-up and syncing projects&lt;/h2&gt;
&lt;p&gt;Being able to recover tmux sessions is great, but what if you want to sync projects between devices, or back them up?&lt;/p&gt;
&lt;p&gt;There are various approaches for this. I store my configuration files in my personal &lt;a href=&#34;https://nextcloud.com&#34; target=&#34;_blank&#34; rel=&#34;noopener noreferrer&#34; &gt;Nextcloud&lt;/a&gt;, which means I can hydrate any new devices with a simple link:&lt;/p&gt;
&lt;div class=&#34;highlight&#34;&gt;&lt;pre tabindex=&#34;0&#34; style=&#34;background-color:#f0f3f3;-moz-tab-size:2;-o-tab-size:2;tab-size:2;&#34;&gt;&lt;code class=&#34;language-shell&#34; data-lang=&#34;shell&#34;&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;ln -s ~/Nextcloud/dotfiles/.config/tmuxinator ~/.config/tmuxinator
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;That way, if I use &lt;code&gt;tmuxinator new&lt;/code&gt; to create a new project configuration it will automatically get synced to my Nextcloud. This approach also works if you use software like &lt;a href=&#34;https://syncthing.net&#34; target=&#34;_blank&#34; rel=&#34;noopener noreferrer&#34; &gt;Syncthing&lt;/a&gt;.&lt;/p&gt;
&lt;p&gt;If you work in a team and want to share your setup through version control, you could also commit the project-specific YAML file to your repo. The &lt;code&gt;tmuxinator start&lt;/code&gt; command will look in &lt;code&gt;./.tmuxinator.yml&lt;/code&gt; before anywhere else, and so this offers a nice way to get your whole team using a consistent setup. However, in my experience the workspace setup can be quite a personal thing!&lt;/p&gt;
&lt;p&gt;If you have any other thoughts for maintaining terminal-based workspace sessions, then I’d love to hear them. Please let me know.&lt;/p&gt;
</content:encoded>
    </item>
    
    <item>
      <title>RSS: include your entire posts in your feeds!</title>
      <link>https://wilw.dev/blog/2021/06/12/rss/</link>
      <pubDate>Sat, 12 Jun 2021 11:05:00 +0000</pubDate>
      
      <guid>https://wilw.dev/blog/2021/06/12/rss/</guid>
      
        <category>100daystooffload</category>
      
        <category>technology</category>
      
        <category>opinion</category>
      
      
      <content:encoded>&lt;p&gt;Recently I’ve noticed that some of the RSS feeds I subscribe to have become more and more restrictive. A post might contain just a title, or perhaps a short snippet or introductory paragraph, with the expectation that I then proceed to follow the link to visit the website itself in order to view the post in full.&lt;/p&gt;
&lt;p&gt;I suppose in many ways that this is similar to distributing podcasts via RSS: the feed contains the podcast title, description, and other metadata, and then a link to download the podcast episode file itself. But this is because podcasts are in audio or video format and cannot be reasonably embedded directly into an XML file.&lt;/p&gt;
&lt;p&gt;But blog posts and articles are primarily &lt;em&gt;text&lt;/em&gt;, for which XML is perfect for transferring.&lt;/p&gt;
&lt;p&gt;Most of the examples I’ve seen are commercial news outlets that probably still make (at least some) income through selling adverts. Although I still disagree with this (we all use ad-blockers anyway), in these cases they have a business objective to get you to their website for their own analytics and to drive their revenues.&lt;/p&gt;
&lt;p&gt;However I have seen some personal text blogs and non-commercial outlets doing the same thing, and if this is intentional I just wonder what the motivation is. Maybe it’s for site analytics? Or maybe the author is worried about the size of the feed’s XML file getting too large?&lt;/p&gt;
&lt;p&gt;If you need analytics of subscribers the author can simply log basic request info to their feed’s XML file download. To prevent the XML file from getting too big authors can simply limit the feed to the most recent &lt;em&gt;n&lt;/em&gt; posts.&lt;/p&gt;
&lt;p&gt;Either way, there are a number of good reasons for allowing your subscribers to retrieve entire post contents in their feeds.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Client familiarity&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;Many people use a familiar client to consume blog posts and articles. For example, I use &lt;a href=&#34;https://reeder.app&#34; target=&#34;_blank&#34; rel=&#34;noopener noreferrer&#34; &gt;Reeder&lt;/a&gt;, which has a fantastic interface that makes reading posts and content very enjoyable. It uses a great readable font, displays images beautifully, respects my system’s dark/light theme, and much more.&lt;/p&gt;
&lt;p&gt;Other people might enjoy different clients, but the point is that this makes the experience consistent across feeds. Therefore, consuming the content is much quicker and feels more natural. If I have to visit your website to view the article then I have to find where the content starts on your particular site, deal with whatever font and colours you choose and have inconsistent layouts across my different subscriptions.&lt;/p&gt;
&lt;p&gt;I often read posts on my phone, and if your website is non-responsive to smaller screen sizes then this is a massive pain. In general, reading your posts via a client is much less invasive on my time and I can concentrate on actually enjoying the content.&lt;/p&gt;
&lt;p&gt;Clients can also make use of accessibility features (like screen readers) in order to make your post available to a wider audience.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Caching&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;When my client refreshes the feed it downloads all the latest unread posts (as well as storing previously-read ones). This means that if I am about to take a flight or get on the tube I know I will have lots of interesting content to read whilst my phone is out of the network’s reach.&lt;/p&gt;
&lt;p&gt;However, if I have to visit your website to view the post then it simply can’t be read. By the time I’ve landed the title has probably been forgotten and I won’t remember to go back through and load it.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Protocol agnosticism&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;RSS is protocol-agnostic in the sense of accessing the content within. For podcasts this may be a link (usually using HTTP) to access the episode file.&lt;/p&gt;
&lt;p&gt;For text feeds, it doesn’t matter what the “source” is: it could be a website, an FTP server, a gemini capsule, or anything else. Maybe, in some cases, there &lt;em&gt;isn’t&lt;/em&gt; an explicit source and RSS is the primary means of distribution?&lt;/p&gt;
&lt;p&gt;Either way, one shouldn’t assume that people always want to access via HTTP, and so including the text content directly in the feed helps to keep it pure and simple.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Trust&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;If I can read your content directly within my own client, it helps build trust. I know you aren’t trying to track my every move and that you care about my ability to read the content.&lt;/p&gt;
&lt;p&gt;I know that you are sharing your content for the sake of the writing or piece itself (perhaps because you enjoy writing or want to share your thoughts), and not in order to drive sales or use patterns to manipulate me into carrying out an action that you want. It also shows you respect user privacy.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Lightweight&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;Distributing text-only versions of your posts is much lighter than having to transfer entire webpage files, CSS, JavaScript, and much more. In fact, if you pay for server space with per-MB billed traffic egress then this could help save you money.&lt;/p&gt;
&lt;p&gt;More and more people in the tech community browse the web without JavaScript enabled anyway, so if your site relies on JS to load then they won’t be able to view your content. Think about the people in the intended audience of your post.&lt;/p&gt;
&lt;h2 id=&#34;conclusion&#34;&gt;Conclusion&lt;/h2&gt;
&lt;p&gt;One can easily argue that “this is my site, and if I write the content then I want people to view it &lt;em&gt;my&lt;/em&gt; way”. This is perfectly fine, and is entirely up to you. This post is more about explaining why these practices might convey quality-of-life enhancements to your readers, and is just my opinion and not a set of rules.&lt;/p&gt;
&lt;p&gt;The web (and internet in general) is great in that it gives everyone a platform to distribute their content in the way they choose. However, since it’s up to others if they choose to read what you post, by making this easier and more accessible to them you can make sure you reach a wider audience.&lt;/p&gt;
</content:encoded>
    </item>
    
    <item>
      <title>City-centre beekeeping</title>
      <link>https://wilw.dev/blog/2021/06/11/beekeeping/</link>
      <pubDate>Fri, 11 Jun 2021 19:00:00 +0000</pubDate>
      
      <guid>https://wilw.dev/blog/2021/06/11/beekeeping/</guid>
      
        <category>100daystooffload</category>
      
        <category>life</category>
      
      
      <content:encoded>&lt;h2 id=&#34;getting-and-installing-the-nuc&#34;&gt;Getting and “installing” the nuc&lt;/h2&gt;
&lt;p&gt;For his birthday a few years back, I bought my (&lt;a href=&#34;https://wilw.dev/blog/2021/06/10/married&#34;&gt;now-&lt;/a&gt;)husband a beehive and a &lt;a href=&#34;https://en.wikipedia.org/wiki/Nuc&#34; target=&#34;_blank&#34; rel=&#34;noopener noreferrer&#34; &gt;honeybee nucleus&lt;/a&gt;.&lt;/p&gt;
&lt;p&gt;Some might see this as a strange gift, especially given that we live close to the city centre. It was certainly a surprise for him, but given his love for animals and science I knew he would like it.&lt;/p&gt;
&lt;p&gt;We were lucky enough to have a relatively large garden, given our location, of around 20 metres in length. Since we didn’t really use the end of the garden much, it was a good location for hive - though other people have successfully kept bees in much smaller areas and on rooftops.&lt;/p&gt;
&lt;p&gt;The hive is an &lt;a href=&#34;https://www.omlet.co.uk/shop/beekeeping/beehaus&#34; target=&#34;_blank&#34; rel=&#34;noopener noreferrer&#34; &gt;Omlet Beehaus&lt;/a&gt;, which features two entrances (one on either end). This means that - using the included divider board - one can keep two separate colonies, and the distance between the entrances is sufficient to ensure the bees know which one is theirs.&lt;/p&gt;
&lt;p&gt;&lt;img src=&#34;https://wilw.dev/media/blog/bees1.jpg&#34; alt=&#34;The beehive and nucelus&#34;&gt;&lt;/p&gt;
&lt;p&gt;&lt;em&gt;The image above shows the Beehaus hive and the nuc box on the floor. The bees are being added to the right-hand side colony and there is a divider board in place.&lt;/em&gt;&lt;/p&gt;
&lt;p&gt;I collected the nuc from a farm in Oxfordshire, England - a few hours’ drive from our home. The nuc box had to be kept cool on the way home and the bees also needed water. It was a little disconcerting travelling with a box full of bees!&lt;/p&gt;
&lt;p&gt;After we got them home, we moved the bees to the hive by transferring the nuc frames (which are shorter than normal brood frames) from the nuc box to the brood box of the Beehaus.&lt;/p&gt;
&lt;h2 id=&#34;the-queen&#34;&gt;The queen&lt;/h2&gt;
&lt;p&gt;Luckily, the queen had already been marked and so she was relatively easy to identify. Each colony has one queen, which is marked by some harmless ink on the back of her body to make her more visible. There is a &lt;a href=&#34;http://beespoke.info/2014/04/01/queen-marking-colours&#34; target=&#34;_blank&#34; rel=&#34;noopener noreferrer&#34; &gt;recognised standard&lt;/a&gt; for marking queens based on the year in which they are born - ours was white for “2016”.&lt;/p&gt;
&lt;p&gt;&lt;img src=&#34;https://wilw.dev/media/blog/bees2.jpg&#34; alt=&#34;Frame of bees and brood&#34;&gt;&lt;/p&gt;
&lt;p&gt;&lt;em&gt;The image above shows a frame of capped brood in our nuc. The white marked queen is visible near the centre bottom of the frame.&lt;/em&gt;&lt;/p&gt;
&lt;p&gt;Queen bees are extremely important for the correct function of a honeybee colony. They lay the eggs that become new bees, they secrete hormones and other scents that control the colony in various ways, they dictate the &lt;em&gt;mood&lt;/em&gt; of the colony, they choose when it’s time to swarm, and much more.&lt;/p&gt;
&lt;p&gt;Without a queen present, the bees will try and create a new queen out of an existing larvae at the right stage of development. Without an egg-laying queen, a colony would eventually die out.&lt;/p&gt;
&lt;p&gt;Our first queen was lovely. The colony was super-calm (we often wouldn’t need a suit for inspections), there was always a healthy brood, and the honey collection was strong even from the start.&lt;/p&gt;
&lt;h2 id=&#34;the-first-few-weeks&#34;&gt;The first few weeks&lt;/h2&gt;
&lt;p&gt;We got the bees at the start of summer. Since a new colony from a nuc takes some time to get up to full-size, it was unlikely that they’d swarm the first year. Swarms take place in the summer months, but only if certain conditions are met.&lt;/p&gt;
&lt;p&gt;This was good, as it gave us a year to get the hang of things before having to deal with a swarm!&lt;/p&gt;
&lt;p&gt;Over the first couple of months, the colony grew in size and we gradually swapped out the nuc frames for full-size brood frames in order to give the bees space to grow (i.e. create the cells for storing eggs, larvae, pollen, and honey).&lt;/p&gt;
&lt;p&gt;The workers (female bees) were out in full-force collecting pollen, and the drones would do their thing (usually nothing) back at the hive.&lt;/p&gt;
&lt;p&gt;Workers can travel a good few miles on a single trip, and gradually travel further out as they learn the local area. Due to this range, it isn’t recommended to move bees within a few miles of their established location, since when they leave the hive there is a good chance they will recognise the area and travel back to where the hive used to be.&lt;/p&gt;
&lt;p&gt;Bees can be quite sensitive to changes in the hive location, with the general rule being to move the hive only less than one metre OR more than six miles.&lt;/p&gt;
&lt;h2 id=&#34;social-factors&#34;&gt;Social factors&lt;/h2&gt;
&lt;p&gt;We worried that the neighbours might be a little nervous about a colony of bees so nearby, but actually they seemed to really enjoy it. They often asked us about it and planted flowers especially!&lt;/p&gt;
&lt;p&gt;This felt really positive given current worldwide efforts in conserving bee populations.&lt;/p&gt;
&lt;p&gt;One of our neighbours also had a pond, which was great as it allowed the bees to drink (they don’t like to drink water from sources too close to their hive).&lt;/p&gt;
&lt;p&gt;Generally, bees aren’t interested in people at all. They would only become aggressive if they feel their colony is under threat, and so finding a bee out in the wild or a few metres away from the hive is pretty safe. Bees usually travel quite quickly about 10 metres above the ground, and would usually only descend when collecting pollen or when returning to the hive.&lt;/p&gt;
&lt;h2 id=&#34;adding-the-supers&#34;&gt;Adding the “supers”&lt;/h2&gt;
&lt;p&gt;We were able to add the shorter “super” frames after a couple of months. It was a long summer, and the bees continued being quite active long into September and beyond.&lt;/p&gt;
&lt;p&gt;Super boxes and frames sit on top of the brood box (you can see two in the photo above), and there is a “queen excluder” board that sits between the supers and the rest of the hive. The excluder features holes that allow workers through to deposit honey into cells in the super frames, but are too small for the queen to get through.&lt;/p&gt;
&lt;p&gt;This is useful for harvesting honey, as the eggs and larvae and pollen stay below in the main brood box. The workers take the excess honey into the super frames, which can be easily withdrawn without disturbing the main hive.&lt;/p&gt;
&lt;p&gt;Bees collect more than enough honey and place most of it in their main brood box, which we do not remove. It is only the excess honey which is taken.&lt;/p&gt;
&lt;h2 id=&#34;first-harvest&#34;&gt;First harvest&lt;/h2&gt;
&lt;p&gt;We got our first harvest late in the summer. Cities are full of lots of different types of flowers and plants, since every garden is different, and the vibrant gardens were clearly very attractive to the bees!&lt;/p&gt;
&lt;p&gt;&lt;img src=&#34;https://wilw.dev/media/blog/bees3.jpg&#34; alt=&#34;A super frame full of capped honey&#34;&gt;&lt;/p&gt;
&lt;p&gt;&lt;em&gt;The image above shows a super frame full of capped honey ready for harvest.&lt;/em&gt;&lt;/p&gt;
&lt;p&gt;We checked the supers every week as part of our normal inspections, and eventually decided that the supers were full enough to harvest. The supers, when full of honey, are heavy, which is very satisfying! The honeycomb structure is effective: a single frame could easily hold several jars’ worth of honey.&lt;/p&gt;
&lt;p&gt;We extracted the honey by carefully scraping the comb away from the foundation sheet. This gave us a bowl full of honey and wax from the comb, which we strained through cloth into jars. We kept the wax and later purified it - we still haven’t done anything with this growing collection of wax!&lt;/p&gt;
&lt;p&gt;Once harvested, the empty frames were re-added to the supers to allow the workers to rebuild the comb and continue honey production.&lt;/p&gt;
&lt;h2 id=&#34;winter&#34;&gt;Winter&lt;/h2&gt;
&lt;p&gt;As the days got colder, the colony began to naturally shrink in size. We removed the supers and gradually reduced the number of frames within the brood box. This helps the bees maintain their own internal temperature during the winter.&lt;/p&gt;
&lt;p&gt;During the winter there is no way for the bees to collect additional food. They can feed off the honey they produced during the warmer months, but we helped them along by feeding them. This can be done by making a sugar solution and placing this in a “contact feeder” upside-down in the hive.&lt;/p&gt;
&lt;p&gt;We checked the bees less during the winter. This helps prevent the cold air getting in when opening the lid.&lt;/p&gt;
&lt;h2 id=&#34;spring-and-swarm-watch&#34;&gt;Spring and swarm watch&lt;/h2&gt;
&lt;p&gt;In spring, the days began to get warmer again, which was marked by more bee activity around the hive entrance. We gradually re-added frames as the colony began to increase in size again.&lt;/p&gt;
&lt;p&gt;As the months progressed we kept an eye out for signs of bee swarming. This can be fiddly business, but there are a number of indicators you can use. The most obvious indication of a colony preparing to swarm is the presence of queen cells in the brood frames.&lt;/p&gt;
&lt;p&gt;Queen cells are created by workers when it is time to create a new queen. It is done by elongating a normal worker (female) brood cell outward and feeding the larva royal jelly (a special kind of honey) as it develops. This jelly makes the bee larva grow bigger than a normal bee and with the characteristics of a queen.&lt;/p&gt;
&lt;p&gt;Queen cells are obvious to see, and usually show near the bottom of the frame. I don’t have any photos myself but there are lots if you search the web.&lt;/p&gt;
&lt;p&gt;Once a queen cell has been capped (i.e. sealed off for its final development), it is likely that the colony will swarm within the next couple of days.&lt;/p&gt;
&lt;h2 id=&#34;swarm-control&#34;&gt;Swarm control&lt;/h2&gt;
&lt;p&gt;Swarms are part of the natrual honeybee colony lifecycle, and usually occur every year in healthy colonies. A swarm signifies the division of a colony: the old queen flies away with most of the colony, leaving some bees behind (workers and nurse bees, as well as drones) along with the soon-to-hatch queen(s).&lt;/p&gt;
&lt;p&gt;A swarm is quite an intense process, and involves lots of loud flying bees as the hive gradually empties. However, a bee swarm is actually pretty safe: the bees are often at their most docile when in a swarm.&lt;/p&gt;
&lt;p&gt;There are many ways one can prevent an all-out swarm. Swarm control is definitely recommended in a city to avoid concerning neighbours.&lt;/p&gt;
&lt;p&gt;Some people clip the wings of the queen to prevent her from being able to fly (the colony won’t fly away without her). Others carry out an “artificial swarm” - this is what we opted for.&lt;/p&gt;
&lt;p&gt;To artificially swarm the bees, we took some of the brood frames of honey and pollen from one half of the Beehaus and placed them in the other half - making sure the queen was also transferred.&lt;/p&gt;
&lt;p&gt;&lt;img src=&#34;https://wilw.dev/media/blog/bees4.jpg&#34; alt=&#34;Swapping frames between the sides of the hive&#34;&gt;&lt;/p&gt;
&lt;p&gt;&lt;em&gt;The image above shows us selecting frames for transfer to the new side of the hive to artifically swarm the colony.&lt;/em&gt;&lt;/p&gt;
&lt;p&gt;This left us with two separate colonies - the existing one containing the “new” bees with a few frames of brood, the queen cells, and some pollen and honey, along with some remaining workers, drones, and nurse bees. Importantly, the new colony does not have a current queen.&lt;/p&gt;
&lt;p&gt;The “swarmed” colony on the other side of the Beehaus contained the existing queen, plus many of her workers, drones, and frames of pollen and honey and - importantly - no queen cells! This will now trick the queen into thinking she has already swarmed: it is a “new” hive, no queen cells around or brood. She can begin the process again of egg laying as before.&lt;/p&gt;
&lt;h2 id=&#34;the-two-colonies&#34;&gt;The two colonies&lt;/h2&gt;
&lt;p&gt;The “swarmed” colony continued to grow as before. The existing queen gradually continued laying to get the colony back up to size. We were able to place supers back on that side not long after.&lt;/p&gt;
&lt;p&gt;In the “new” queenless colony we removed all but two of the queen cells. After a few days she emerged and went on her mating flight. During this flight she mated with a drone and returned to the hive. Shortly after she began laying and started her other queenly duties.&lt;/p&gt;
&lt;p&gt;We were lucky in that the process worked quite smoothly. Sometimes, the first queen to emerge will explore the hive and sting any still-to-emerge queens through their cells to ensure she is the only queen. If two queens hatch at similar times, they can fight. Either way, there can only be at most one queen. If the queening is unsuccessful for any reason (e.g. they die when fighting or if they fail to emerge from their cells) the colony can be requeened by purchasing a new queen from a farm.&lt;/p&gt;
&lt;h2 id=&#34;uniting-the-colonies&#34;&gt;Uniting the colonies&lt;/h2&gt;
&lt;p&gt;Having two colonies is fine if you are a commercial beekeeper with lots of space. However, colony division would mean that we would eventually have four, eight, sixteen colonies, and so on as they continued to swarm and divide year on year.&lt;/p&gt;
&lt;p&gt;We only really wanted one colony, and having the spare half of the Beehaus is useful for ongoing bee maintenance. As such, we opted to unite the colonies.&lt;/p&gt;
&lt;p&gt;Uniting a colony involves getting rid of one queen and then gradually bringing the colonies together. Given the success of the first queen we had (both in terms of bee temperament and honey production), we chose to keep our old faithful.&lt;/p&gt;
&lt;p&gt;To unite the colonies we first of all removed the new queen (I won’t go into the details here). We then modified the divider board between the two halves of the hive to add a sheet of paper. The bees belonging to the new queen would recognise an entirely different set of scents and hormones, and require a slow introduction to our older queen.&lt;/p&gt;
&lt;p&gt;Adding the paper allows the various chemicals to gradually filter through as the bees on either side eat through. This gradual process allows bees on both sides to become accustomed to each other and to allow the now-queenless bees to get used to their new queen.&lt;/p&gt;
&lt;p&gt;Eventually the paper is gone, and we were able to move the frames all back to one side of the Beehaus as before.&lt;/p&gt;
&lt;h2 id=&#34;the-second-year&#34;&gt;The second year&lt;/h2&gt;
&lt;p&gt;After the swarming and re-uniting, the bees carried on much the same as the first year. We had several great harvests from some very productive bees!&lt;/p&gt;
&lt;h2 id=&#34;swarm-un-control&#34;&gt;Swarm un-control&lt;/h2&gt;
&lt;p&gt;In our third year, we again looked out for the signs of swarming but we must have missed a trick. One day I noticed a huge amount of activity around the hive and realised what must have happened. There could have been less obvious queen cells present, or perhaps the existing queen just decided to leave early.&lt;/p&gt;
&lt;p&gt;Either way, the bees were definitely properly swarming. Luckily, it was a weekday and so nearly everyone around was at work or at school (we worked from home at the time). We could see the bees congregating around a tree in a nearby garden - this must have been where the queen was.&lt;/p&gt;
&lt;p&gt;&lt;img src=&#34;https://wilw.dev/media/blog/bees5.jpg&#34; alt=&#34;The bees swarming and gathering around a tree trunk&#34;&gt;&lt;/p&gt;
&lt;p&gt;&lt;em&gt;The image above shows bees swarming and a mass of bees gathering around a small tree trunk.&lt;/em&gt;&lt;/p&gt;
&lt;p&gt;The swarm was very noisy. We knew we didn’t really have long to try and get the queen and the rest of the bees back (they would return home once they knew the queen was no longer there). We didn’t want to just go into someone else’s garden without permission, and so I went round to introduce myself. Luckily they were home, and were very understanding.&lt;/p&gt;
&lt;p&gt;They let us into their house and garden and we were able to scoop the mass of bees into a shoebox. There was too much activity to identify the queen, but we just &lt;em&gt;hoped&lt;/em&gt; we had her.&lt;/p&gt;
&lt;p&gt;&lt;img src=&#34;https://wilw.dev/media/blog/bees6.jpg&#34; alt=&#34;Recovering the swarm&#34;&gt;&lt;/p&gt;
&lt;p&gt;&lt;em&gt;We transported the bees over the wall to avoid carrying them through the house.&lt;/em&gt;&lt;/p&gt;
&lt;p&gt;&lt;img src=&#34;https://wilw.dev/media/blog/bees7.jpg&#34; alt=&#34;Carrying the bees in a shoebox&#34;&gt;&lt;/p&gt;
&lt;p&gt;&lt;em&gt;Success!&lt;/em&gt;&lt;/p&gt;
&lt;p&gt;We were able to get the box back to the hive and emptied them back inside. We removed any of the queen cells we could find. We closed the lid and hoped the ordeal was over.&lt;/p&gt;
&lt;p&gt;Luckily, the activity died down over the rest of the day, and the bees resumed normal activity over the next few days. All of the drama was clearly enough to convince the queen that she had already swarmed, and she was happy to resume business as usual.&lt;/p&gt;
&lt;h2 id=&#34;continuing-on&#34;&gt;Continuing on&lt;/h2&gt;
&lt;p&gt;The rest of our beekeeping adventure was much less exciting. We got stung a few times (mainly through the gloves), but nothing serious or anything to write home about. We now have a new queen from a more recent swarm.&lt;/p&gt;
&lt;p&gt;Other than the swarm mistake we made, the bees did not bother our neighbours. They enjoyed having the bees around and actively engaged with us and them.&lt;/p&gt;
&lt;p&gt;We definitely learned a lot (and still continue to). It feels great to help support the bee preservation movement, and they are probably some of the most interesting animals on the planet.&lt;/p&gt;
&lt;p&gt;If you are interested in bees, have some space and some easy-going neighbours, I can definitely recommend giving it a try. You can also join local beekeeping societies to join a wider community, and everyone is very supportive!&lt;/p&gt;
</content:encoded>
    </item>
    
    <item>
      <title>Married</title>
      <link>https://wilw.dev/blog/2021/06/10/married/</link>
      <pubDate>Thu, 10 Jun 2021 08:04:00 +0000</pubDate>
      
      <guid>https://wilw.dev/blog/2021/06/10/married/</guid>
      
        <category>100daystooffload</category>
      
        <category>life</category>
      
      
      <content:encoded>&lt;p&gt;Just a quick post to say that I recently got married! By coincidence the event was three years to the day after our engagement.&lt;/p&gt;
&lt;p&gt;It was a lovely day - great weather and really nice to see those that could attend. Hopefully we’ll get a chance to go away later in the year if/when things start opening up again 😁&lt;/p&gt;
</content:encoded>
    </item>
    
    <item>
      <title>I can&#39;t play games anymore</title>
      <link>https://wilw.dev/blog/2021/06/02/gaming/</link>
      <pubDate>Wed, 02 Jun 2021 21:00:00 +0000</pubDate>
      
      <guid>https://wilw.dev/blog/2021/06/02/gaming/</guid>
      
        <category>100daystooffload</category>
      
        <category>life</category>
      
      
      <content:encoded>&lt;h2 id=&#34;growing-up-and-the-warcraft-years&#34;&gt;Growing up and the “Warcraft years”&lt;/h2&gt;
&lt;p&gt;In my earlier years I was fairly into gaming. I was definitely only ever a “casual gamer” in the scheme of things today, but I would play at least a small amount of &lt;em&gt;something&lt;/em&gt; most days.&lt;/p&gt;
&lt;p&gt;When I was young it was mainly those games based on Nintendo platforms - Super Mario, Mariokart, Super Smash Bros, etc. These were great with friends and were the kind of games (along with their various sequels) that we could play over again and for many years to come. Pokemon was also a big hit for me, which would continue on through the consoles.&lt;/p&gt;
&lt;p&gt;As I moved into my early teens, strategy games became more my thing. My elder brother introduced me to &lt;a href=&#34;https://en.wikipedia.org/wiki/Warcraft&#34; target=&#34;_blank&#34; rel=&#34;noopener noreferrer&#34; &gt;Warcraft&lt;/a&gt; and I would play and re-play the various campaigns of Warcraft I and Warcraft II.&lt;/p&gt;
&lt;p&gt;When Warcraft III became more relevant to me I would sink hours into playing LAN games with friends and siblings. This was also my first proper foray into online gaming (through &lt;a href=&#34;https://en.wikipedia.org/wiki/Battle.net&#34; target=&#34;_blank&#34; rel=&#34;noopener noreferrer&#34; &gt;Battle.net&lt;/a&gt;).&lt;/p&gt;
&lt;p&gt;I didn’t really touch the other Blizzard games (Starcraft and Diablo) too much, but did get involved with a few other RPG- and simulation-type ones (The Sims, Rollercoaster Tycoon, and others).&lt;/p&gt;
&lt;p&gt;When &lt;a href=&#34;https://en.wikipedia.org/wiki/World_of_Warcraft&#34; target=&#34;_blank&#34; rel=&#34;noopener noreferrer&#34; &gt;World of Warcraft&lt;/a&gt; was released in my early-mid teens, this was a bit of a game-changer. To me, it was the perfect combination of success/reward, social factors, depth of story, and (at the time) a huge world to explore.&lt;/p&gt;
&lt;p&gt;It quickly became my only game and a sort of addiction. My friends and siblings all played it, and it would end up replacing other online hang-out spaces of the era (MSN Messenger). As such, it was much more than just a game to me - and I think many would feel the same. The nostalgia for those early WoW years has always been on my mind - though unfortunately was not really rekindled even when WoW Classic was much later released.&lt;/p&gt;
&lt;h2 id=&#34;moving-to-university&#34;&gt;Moving to university&lt;/h2&gt;
&lt;p&gt;I was still playing WoW for several hours each day, even when I hit my late teens and my A-Level exams. I was lucky enough to scrape the grades needed (despite a distinct lack of study and revision!) and headed to university.&lt;/p&gt;
&lt;p&gt;This is when things began to change a litte. I suddenly had new responsibilities, new friends, and new experiences. Gaming took a rather sudden back-seat to everything else that was going on in my life - new people, exploring, partying (and learning, I guess).&lt;/p&gt;
&lt;p&gt;As I progressed through university, I would still play sporadically. But this would only be in my “home” life - the life I had when I visited my family and younger siblings. They were still at an age of no responsibility, and so gaming would be a natural pastime. We would play &lt;a href=&#34;https://en.wikipedia.org/wiki/Minecraft&#34; target=&#34;_blank&#34; rel=&#34;noopener noreferrer&#34; &gt;Minecraft&lt;/a&gt;, &lt;a href=&#34;https://en.wikipedia.org/wiki/Garry%27s_Mod&#34; target=&#34;_blank&#34; rel=&#34;noopener noreferrer&#34; &gt;Garry’s Mod&lt;/a&gt;, and other similar games.&lt;/p&gt;
&lt;p&gt;If we went on family holidays together I might buy the latest Pokemon RPG to play together, but this would quickly be forgotten afterwards as I got back to my main life.&lt;/p&gt;
&lt;p&gt;When I play games with family I really enjoy it, and since in these cases I am on holiday anyway it feels like real leisure time.&lt;/p&gt;
&lt;h2 id=&#34;now&#34;&gt;Now&lt;/h2&gt;
&lt;p&gt;When I left university to begin full-time work my life changed again. I now had even less time, and I felt no real motivation to game even in my spare time. The last real game I played was Animal Crossing: New Horizons when that hit the Switch during last year’s lockdown, but that’s it really aside from occasional bouts with my siblings.&lt;/p&gt;
&lt;p&gt;This post sounds pretty downbeat, but I don’t mean for it to at all. I actually love games as a concept - they are a work of art and often reflect years of creative input across gameplay, story, graphics, music, and more.&lt;/p&gt;
&lt;p&gt;I love that small indie (and solo) developers can be given a platform to sell from that enables them to compete with the larger studios. I am fascinated by all the different genres, and the new twists we see on these year on year.&lt;/p&gt;
&lt;p&gt;I still follow lots of gaming news and am interested in keeping up to date with developments.&lt;/p&gt;
&lt;p&gt;It’s weird - I just don’t have the motivation to play games myself anymore.&lt;/p&gt;
&lt;p&gt;I know games should be used as an opportunity to wind down and relax, but every time I do my mind protests, “you should be doing something more productive”. I don’t know why this is, and why I can’t seem to shut down any more, but there it is. I can’t really sit and watch TV either.&lt;/p&gt;
&lt;p&gt;I would be more comfortable spending my “downtime” continuing working, learning, and improving myself. This all sounds very noble, but it’s actually pretty frustrating. I enjoy learning and working, but understand the importance of being able to shut down and relax once in a while.&lt;/p&gt;
&lt;p&gt;Does anyone have any experience with this or have any tips?&lt;/p&gt;
</content:encoded>
    </item>
    
    <item>
      <title>The H.G. Wells Classic Collection</title>
      <link>https://wilw.dev/blog/2021/05/26/h-g-wells/</link>
      <pubDate>Wed, 26 May 2021 21:50:00 +0000</pubDate>
      
      <guid>https://wilw.dev/blog/2021/05/26/h-g-wells/</guid>
      
        <category>100daystooffload</category>
      
        <category>book</category>
      
      
      <content:encoded>&lt;p&gt;The &lt;a href=&#34;https://www.goodreads.com/book/show/7069333-h-g-wells-classic-collection-i&#34; target=&#34;_blank&#34; rel=&#34;noopener noreferrer&#34; &gt;Classic Collection&lt;/a&gt; of &lt;a href=&#34;https://www.goodreads.com/author/show/880695.H_G_Wells&#34; target=&#34;_blank&#34; rel=&#34;noopener noreferrer&#34; &gt;H.G. Wells&lt;/a&gt; novels contains five well-known stories: &lt;em&gt;The War of the Worlds&lt;/em&gt;, &lt;em&gt;The First Men in the Moon&lt;/em&gt;, &lt;em&gt;The Time Machine&lt;/em&gt;, &lt;em&gt;The Invisible Man&lt;/em&gt;, and &lt;em&gt;The Island of Doctor Moreau&lt;/em&gt;.&lt;/p&gt;
&lt;p&gt;&lt;img src=&#34;https://wilw.dev/media/blog/h_g_wells.jpg&#34; alt=&#34;The cover of the H.G. Wells Classic Collection&#34;&gt;&lt;/p&gt;
&lt;p&gt;Despite the fame of these novels, I had never read any of them until I recently listened to them via the &lt;a href=&#34;https://www.audible.co.uk/pd/HG-Wells-The-Science-Fiction-Collection-Audiobook/B07PP8N213&#34; target=&#34;_blank&#34; rel=&#34;noopener noreferrer&#34; &gt;audiobook version&lt;/a&gt;, which was excellently narrated by the likes of David Tennant, Hugh Bonneville, and others.&lt;/p&gt;
&lt;p&gt;Wells is famous for being an early sci-fi writer (indeed, he is known as the ‘Father of Science Fiction’), with his first such book - &lt;em&gt;The Time Machine&lt;/em&gt; - being published in 1895.&lt;/p&gt;
&lt;p&gt;Books in this collection have formed the basis of both classic and modern films - perhaps most notably &lt;em&gt;The War of the Worlds&lt;/em&gt; (published as a novel in 1898) and &lt;em&gt;The Invisible Man&lt;/em&gt; (1897).&lt;/p&gt;
&lt;p&gt;I’m a big lover of science fiction and it seemed sensible to go back to what can be seen as its birthplace. I really enjoyed the collection - each novel is thought provoking and engaging. The novels aren’t long and the stories progress quickly, which helps to add to the excitement.&lt;/p&gt;
&lt;p&gt;Although quite light-hearted seeming (in a classic Wellsian and British fashion), and often humorously written, the stories do go into interesting detail around the science and methods behind the phenomena that form the plots. From discoveries of new materials in &lt;em&gt;The First Men in the Moon&lt;/em&gt;, descriptions of interesting physics in &lt;em&gt;The War of the Worlds&lt;/em&gt;, and the effects of chemicals on human physiology in &lt;em&gt;The Invisible Man&lt;/em&gt;, Wells certainly explores the scientific depths whilst also leaving some to the readers’ imaginations.&lt;/p&gt;
&lt;p&gt;In many of the books there is a significant level of human self-reflection as part of the story, which I think definitely helps place the stories ahead of their time. Whether talking about alien life, or life at a different point in time, there is definitely a sense of Wells projecting the position of humans in their new contexts, and the characters spend time either thinking or talking about themselves, their achievements (or failures), which gives the notion of humility with regard to human ability.&lt;/p&gt;
&lt;p&gt;To me, &lt;em&gt;The Island of Doctor Moreau&lt;/em&gt; stands out as being a little different from the others. Although it is certainly science fiction and is a well-known and acclaimed novel, I found the story’s concepts a little strange and did not enjoy it as much as the rest of the collection.&lt;/p&gt;
&lt;p&gt;Either way, if you are interested in science fiction I would strongly recommend this collection of novels and enjoy the basis of - at the time - what would become countless further books and films often based on the same ideas and similar concepts.&lt;/p&gt;
</content:encoded>
    </item>
    
    <item>
      <title>The networking mall</title>
      <link>https://wilw.dev/blog/2021/05/23/port-mall/</link>
      <pubDate>Sun, 23 May 2021 15:04:00 +0000</pubDate>
      
      <guid>https://wilw.dev/blog/2021/05/23/port-mall/</guid>
      
        <category>100daystooffload</category>
      
        <category>technology</category>
      
      
      <content:encoded>&lt;p&gt;Someone non-technical recently asked me the question, “what actually &lt;em&gt;is&lt;/em&gt; a server?”. They knew it was just a type of computer that runs &lt;em&gt;somewhere&lt;/em&gt; that can be accessible over the internet, but they were interested in how they differ from “normal” computers.&lt;/p&gt;
&lt;p&gt;The conversation moved on to how these computers can make several different functions available at the same time over the network, which brought us on to the topic of services and network ports.&lt;/p&gt;
&lt;p&gt;I was considering a few analogies to best describe the concept of services and ports, and then began talking about shopping malls.&lt;/p&gt;
&lt;h1 id=&#34;shopping-malls-and-servers&#34;&gt;Shopping malls and servers&lt;/h1&gt;
&lt;p&gt;A single shopping mall allows visitors to interact with a large range of different shops and services - such as stores, restaurants, post offices, vending machines, car parks, and more. A single shopping mall is a bit like a computer (or server).&lt;/p&gt;
&lt;p&gt;Each unit (that hosts a service) within the mall is usually numbered, like houses on a street. For example, a specific restaurant might be given the number &lt;code&gt;2500&lt;/code&gt; within the mall. This allows for each service to be addressed uniquely for easier discovery (e.g. for delivering mail or packages). Although each service can be complex and provide a range of functionality, there can only be one service available at each service number.&lt;/p&gt;
&lt;p&gt;If, for example, I wanted to visit the post office in the mall I might visit unit number &lt;code&gt;110&lt;/code&gt;. Here I can prove my identity in order to receive mail that they may be holding for me. Bringing this back to severs, this concept is similar to that of using &lt;a href=&#34;https://en.wikipedia.org/wiki/Post_Office_Protocol&#34; target=&#34;_blank&#34; rel=&#34;noopener noreferrer&#34; &gt;POP (Post Office Protocol) for retrieving email&lt;/a&gt; from a mail server; I connect to (typically) port &lt;code&gt;110&lt;/code&gt; on the mail server, authenticate, and then I can download the messages.&lt;/p&gt;
&lt;p&gt;If I wanted to know the time, I might choose to visit an exhibition of old fashioned watches that happens to be on display in unit &lt;code&gt;37&lt;/code&gt;. Here, I can’t interact with the service in a way other than viewing the time (and appreciating the watches), and each person can only stay for a short while. Similarly, in computing, if I connected to port &lt;code&gt;37&lt;/code&gt; via TCP of a server running the appropriate &lt;a href=&#34;https://en.wikipedia.org/wiki/Time_Protocol&#34; target=&#34;_blank&#34; rel=&#34;noopener noreferrer&#34; &gt;Time Protocol service&lt;/a&gt; I should simply receive back the current time.&lt;/p&gt;
&lt;p&gt;If I happened to work in managing the mall, I might visit unit &lt;code&gt;22&lt;/code&gt; - the manager’s office (equivalent to connecting to a server via &lt;a href=&#34;https://en.wikipedia.org/wiki/Secure_Shell_Protocol&#34; target=&#34;_blank&#34; rel=&#34;noopener noreferrer&#34; &gt;SSH&lt;/a&gt; on port &lt;code&gt;22&lt;/code&gt;) and remain there all day until I finish work.&lt;/p&gt;
&lt;p&gt;The analogies can go much further. The essential thing is that - in computing - I can send traffic over a supported protocol to a specific port in able to interact with the type of service at that port. Some services (like the time protocol one above) might just send a response and then close the connection, whereas others (such as SSH) allow for an ongoing connection to be maintained in order to support a rich and feature-ful experience.&lt;/p&gt;
&lt;p&gt;Although many malls have their manager’s office at unit &lt;code&gt;22&lt;/code&gt;, this is just convention and is not a requirement. The SSH daemon (the service that handles the SSH connection) can run on a different port if so desired. Similarly, libraries are often available at unit &lt;code&gt;80&lt;/code&gt; in many malls - however in some malls there may be multiple libraries available at a range of different unit numbers (and maybe an extra secure library in unit &lt;code&gt;443&lt;/code&gt;).&lt;/p&gt;
&lt;p&gt;Some malls may have a watch exhibition, but it has been closed by the managers (still sitting in unit &lt;code&gt;22&lt;/code&gt;). Since I can’t get in, I am unable to view the current time even if the exhibition itself still exists (I may not even know there &lt;em&gt;is&lt;/em&gt; a watch exhibition).&lt;/p&gt;
&lt;p&gt;Other malls may not have a watch exhibition on at all. If I visited unit &lt;code&gt;37&lt;/code&gt; of these types of malls it would probably be closed. If the unit happens to be open for some reason, the unit would just be empty and I would not be able to receive the service or interact with it in any way.&lt;/p&gt;
&lt;p&gt;Lots of malls recruit security guards to protect the entrance (and exit) of each unit. These guards (the “firewall”) ensure that visitors are allowed into the unit in order to receive the service (even if the unit is open) - perhaps by verifying their proof of address (source IP) - and turn people away if they don’t fulfil the requirements. The firewall guards may also prevent people from leaving the unit.&lt;/p&gt;
&lt;p&gt;If someone keeps trying to repeatedly enter a guarded unit without the appropriate information, they might get banned (either temporarily or permanently).&lt;/p&gt;
&lt;p&gt;Additionally, some units may only admit staff that work in other units of the same mall - this could be done by issuing new rules for the firewall guards or perhaps there is a non-public back corridor connecting the units that only mall staff can use (the loopback interface).&lt;/p&gt;
&lt;p&gt;If the mall is closed completely, then I can’t reach any of the ports or receive any service. For example, if the server is currently turned off or disconnected from the network.&lt;/p&gt;
&lt;h2 id=&#34;some-differences&#34;&gt;Some differences&lt;/h2&gt;
&lt;p&gt;Of course, the mall vs. server anaology isn’t perfect. Most servers only have a small handful of ports open at a given time, and these would be heavily restricted with firewalls and other network protections.&lt;/p&gt;
&lt;p&gt;Equally, when someone does visit a server, they usually do so with one goal in mind (e.g. to download mail OR retrieve web content). In reality, visitors may spend a few hours in a mall and visit a large number of different shops and services.&lt;/p&gt;
&lt;p&gt;However, I find this analogy an interesting and useful way to describe some of the basic networking principles.&lt;/p&gt;
</content:encoded>
    </item>
    
    <item>
      <title>How I back-up my personal server</title>
      <link>https://wilw.dev/blog/2021/05/18/b2-backups/</link>
      <pubDate>Tue, 18 May 2021 22:13:00 +0000</pubDate>
      
      <guid>https://wilw.dev/blog/2021/05/18/b2-backups/</guid>
      
        <category>100daystooffload</category>
      
        <category>technology</category>
      
        <category>selfhost</category>
      
      
      <content:encoded>&lt;p&gt;For a couple of years now I have been using a self-hosted &lt;a href=&#34;https://nextcloud.com&#34; target=&#34;_blank&#34; rel=&#34;noopener noreferrer&#34; &gt;Nextcloud&lt;/a&gt; as a replacement for iCloud and Google Drive. I won’t go into the details as to why (especially given the additional upkeep and other overheads required), as this has been covered before - but mainly it’s about maintaining control over my data.&lt;/p&gt;
&lt;p&gt;I use a cloud VPS to host my Nextcloud instance - rented from &lt;a href=&#34;https://www.linode.com&#34; target=&#34;_blank&#34; rel=&#34;noopener noreferrer&#34; &gt;Linode&lt;/a&gt;, whom I can certainly recommend if you’re looking for a good VPS provider - and since starting my Nextcloud journey I have begun hosting a number of additional services on the same server. For example, &lt;a href=&#34;https://www.freshrss.org&#34; target=&#34;_blank&#34; rel=&#34;noopener noreferrer&#34; &gt;FreshRSS&lt;/a&gt; (which I consume using &lt;a href=&#34;https://www.reederapp.com&#34; target=&#34;_blank&#34; rel=&#34;noopener noreferrer&#34; &gt;Reeder&lt;/a&gt;), &lt;a href=&#34;https://www.monicahq.com&#34; target=&#34;_blank&#34; rel=&#34;noopener noreferrer&#34; &gt;Monica&lt;/a&gt;, &lt;a href=&#34;https://gitea.com&#34; target=&#34;_blank&#34; rel=&#34;noopener noreferrer&#34; &gt;Gitea&lt;/a&gt;, a &lt;a href=&#34;https://matrix.org&#34; target=&#34;_blank&#34; rel=&#34;noopener noreferrer&#34; &gt;Matrix server&lt;/a&gt;, and more.&lt;/p&gt;
&lt;p&gt;Considering the pervasiveness this one machine has with respect to my data and day-to-day life, and the impact it would have if I were to lose access to it, having backups for it is crucial.&lt;/p&gt;
&lt;h2 id=&#34;3-2-1-backup&#34;&gt;3, 2, 1, Backup!&lt;/h2&gt;
&lt;p&gt;Linode offers a backup service for servers, which takes periodic snapshots in order to enable easy recovery of a service, or lost data. That’s one layer, but what happens if Linode itself experiences problems or if I lose access to my account for any reason? Having all of the live data and backups tied to a single provider was definitely a worry for me.&lt;/p&gt;
&lt;p&gt;Many people follow the “3-2-1” rule for backups. This strategy is concerned with - for any piece of data - having at least three copies of that data, two of which stored locally but on different media, and another copy somewhere else (geographically separate).&lt;/p&gt;
&lt;p&gt;Enabling Linode backups allows me to comply with the “3-2” bit of the rule. However, by stopping at this point there is no additional off-site backup in case of catastrophic failure.&lt;/p&gt;
&lt;h2 id=&#34;finding-my-1&#34;&gt;Finding my “1”&lt;/h2&gt;
&lt;p&gt;In order to fully meet the needs of the 3-2-1 strategy, I needed to find a solution for maintaining off-site backups. Additionally, this wouldn’t be a one-time backup; ideally I’d need something that could at least back things up on a daily basis (if not more frequently).&lt;/p&gt;
&lt;p&gt;I began researching solutions, but it wasn’t long until I settled on &lt;a href=&#34;https://www.backblaze.com/cloud-storage&#34; target=&#34;_blank&#34; rel=&#34;noopener noreferrer&#34; &gt;Backblaze B2&lt;/a&gt; - an S3-compatible object storage solution that has great GB/$ pricing. Side note: Linode also offers S3-compatible &lt;a href=&#34;https://www.linode.com/products/object-storage&#34; target=&#34;_blank&#34; rel=&#34;noopener noreferrer&#34; &gt;object storage&lt;/a&gt;, but that wouldn’t help me in this scenario as it’d still be managed by the same provider.&lt;/p&gt;
&lt;p&gt;B2 is cheaper than S3 itself, and also has the benefit of not having to maintain a complex AWS account for simple personal projects.&lt;/p&gt;
&lt;h2 id=&#34;setting-up-backups-to-b2&#34;&gt;Setting up backups to B2&lt;/h2&gt;
&lt;p&gt;Setting up the backups involved a few simple steps:&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;Creating a new Backblaze B2 account&lt;/li&gt;
&lt;li&gt;Setting-up a bucket on B2&lt;/li&gt;
&lt;li&gt;Writing a script to automate the backup to the B2 bucket&lt;/li&gt;
&lt;/ol&gt;
&lt;h3 id=&#34;1-create-a-backblaze-account&#34;&gt;1. Create a Backblaze account&lt;/h3&gt;
&lt;p&gt;You get 10GB free on Backblaze. Head over to &lt;a href=&#34;https://www.backblaze.com/b2/sign-up.html&#34; target=&#34;_blank&#34; rel=&#34;noopener noreferrer&#34; &gt;the sign-up page&lt;/a&gt; in order to create your account.&lt;/p&gt;
&lt;h3 id=&#34;2-set-up-your-bucket&#34;&gt;2. Set-up your bucket&lt;/h3&gt;
&lt;p&gt;Once you’ve got your account and have verified everything, go to the “Buckets” tab of your Backblaze account’s UI, and click “Create a bucket”. This will open up a dialog.&lt;/p&gt;
&lt;p&gt;Enter a unique name for your bucket and ensure you mark files as “private”. I also turned on default encryption for an extra level of security. When ready, click “Create bucket”.&lt;/p&gt;
&lt;p&gt;&lt;img src=&#34;https://wilw.dev/media/blog/b2-backups-1.png&#34; alt=&#34;The &amp;ldquo;create bucket&amp;rdquo; interface&#34;&gt;&lt;/p&gt;
&lt;p&gt;Since we will be periodically backing-up data to this bucket, the bucket will quickly take up more and more space (and cost you more too). As such, I recommend adding a lifecycle rule to tell B2 to automatically delete “old” backup files. To do so, click the “Lifecycle Settings” option on your new bucket, and configure how long you want to keep old file versions around for (I used 10 days):&lt;/p&gt;
&lt;p&gt;&lt;img src=&#34;https://wilw.dev/media/blog/b2-backups-2.png&#34; alt=&#34;The &amp;ldquo;lifecycle settings&amp;rdquo; interface&#34;&gt;&lt;/p&gt;
&lt;p&gt;Finally, we need to create some credentials that will enable the backup system to write files to the bucket. Go to the “App Keys” tab of your B2 dashboard, and click “Add a New Application Key”. On this dialog, name your key and ensure this key can only write files to the bucket. You may also want to restrict this key to only work with your specified bucket.&lt;/p&gt;
&lt;p&gt;&lt;img src=&#34;https://wilw.dev/media/blog/b2-backups-3.png&#34; alt=&#34;The &amp;ldquo;new application key&amp;rdquo; interface&#34;&gt;&lt;/p&gt;
&lt;p&gt;Make a note of the &lt;code&gt;keyID&lt;/code&gt; and &lt;code&gt;applicationKey&lt;/code&gt; that will be displayed (as well as your bucket’s name and the “endpoint” shown on your bucket), as you’ll need these later.&lt;/p&gt;
&lt;h3 id=&#34;3-write-a-backup-script&#34;&gt;3. Write a backup script&lt;/h3&gt;
&lt;p&gt;Backblaze does have an API for managing buckets and files, but using this (especially for larger files) felt overly complex. Since B2 is S3-compatible, we can just make use of standard S3 tools, such as &lt;code&gt;awscli&lt;/code&gt;.&lt;/p&gt;
&lt;p&gt;As such, my script for backups simply creates a tarball containing all of the directories and files I want to backup, and then sends it to B2. This can be as simple as the following:&lt;/p&gt;
&lt;div class=&#34;highlight&#34;&gt;&lt;pre tabindex=&#34;0&#34; style=&#34;background-color:#f0f3f3;-moz-tab-size:2;-o-tab-size:2;tab-size:2;&#34;&gt;&lt;code class=&#34;language-shell&#34; data-lang=&#34;shell&#34;&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;&lt;span style=&#34;color:#099&#34;&gt;#!/bin/bash
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;&lt;span style=&#34;color:#099&#34;&gt;&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;tar --warning&lt;span style=&#34;color:#555&#34;&gt;=&lt;/span&gt;no-file-changed -czf /tmp/backup.tar.gz /first/path /second/path
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;aws s3 cp /tmp/backup.tar.gz &lt;span style=&#34;color:#c30&#34;&gt;&#34;s3://&lt;/span&gt;&lt;span style=&#34;color:#033&#34;&gt;$BUCKET&lt;/span&gt;&lt;span style=&#34;color:#c30&#34;&gt;/backup.tar.gz&#34;&lt;/span&gt; --endpoint-url &lt;span style=&#34;color:#c30&#34;&gt;&#34;&lt;/span&gt;&lt;span style=&#34;color:#033&#34;&gt;$ENDPOINT&lt;/span&gt;&lt;span style=&#34;color:#c30&#34;&gt;&#34;&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;Before running the script, ensure that the following environment variables are set:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;code&gt;AWS_ACCESS_KEY_ID&lt;/code&gt; (set to the &lt;code&gt;keyID&lt;/code&gt; of the B2 key you created)&lt;/li&gt;
&lt;li&gt;&lt;code&gt;AWS_SECRET_ACCESS_KEY&lt;/code&gt; (set to the &lt;code&gt;applicationKey&lt;/code&gt; of the B2 key)&lt;/li&gt;
&lt;li&gt;&lt;code&gt;BUCKET&lt;/code&gt; (the unique name of the bucket you created on B2)&lt;/li&gt;
&lt;li&gt;&lt;code&gt;ENDPOINT&lt;/code&gt; (the endpoint shown on your bucket on the B2 UI: similar to &lt;code&gt;https://s3.eu-central-003.backblazeb2.com&lt;/code&gt;)&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;If these are correctly set (and dependencies like &lt;code&gt;awscli&lt;/code&gt; are installed), you should be able to mark the script as executable and then run it to backup the directories &lt;code&gt;/first/path&lt;/code&gt; and &lt;code&gt;/second/path&lt;/code&gt; (obviously change these to real paths on your server, and you can always add more).&lt;/p&gt;
&lt;p&gt;You can verify the upload was successful by browsing the bucket on the B2 interface. Please note it can sometimes take a few minutes for files to show up!&lt;/p&gt;
&lt;p&gt;&lt;em&gt;Note: I use the &lt;code&gt;--warning=no-file-changed&lt;/code&gt; flag to prevent tar from warning about files that change during the tarball creation process (this happens to me because I backup my Matrix server files too, which change quite frequently as new messages arrive).&lt;/em&gt;&lt;/p&gt;
&lt;h2 id=&#34;automatic-backups&#34;&gt;Automatic backups&lt;/h2&gt;
&lt;p&gt;The above setup is useful for one-off backups, but I wanted to automate the process. This could be as simple as a cron job, but I like Dockerizing things (this makes the environment variables easier to manage too).&lt;/p&gt;
&lt;p&gt;To see my approach to automating the backup, feel free to clone and use the Docker image at &lt;a href=&#34;https://git.wilw.dev/wilw/server-backup&#34; target=&#34;_blank&#34; rel=&#34;noopener noreferrer&#34; &gt;this repository&lt;/a&gt;.&lt;/p&gt;
</content:encoded>
    </item>
    
    <item>
      <title>Running</title>
      <link>https://wilw.dev/blog/2021/05/12/running/</link>
      <pubDate>Wed, 12 May 2021 19:20:00 +0000</pubDate>
      
      <guid>https://wilw.dev/blog/2021/05/12/running/</guid>
      
        <category>100daystooffload</category>
      
        <category>life</category>
      
      
      <content:encoded>&lt;h2 id=&#34;the-effects-of-working-from-home&#34;&gt;The effects of working from home&lt;/h2&gt;
&lt;p&gt;The UK went into its first proper COVID-induced lockdown back around March time last year. At this time, our company locked its office doors and we all began working from home. We’re now all still working remotely about 14 months later and will continue to do so for the forseeable future.&lt;/p&gt;
&lt;p&gt;Before we closed the office, I used to walk across my city - Cardiff - to get to work. It’s about a 3km walk, which would take me about 30 minutes to walk each way. I enjoyed the walk - I could stop for coffee on the way through, and the distance meant I could take different routes on different days if I wanted a change of scene.&lt;/p&gt;
&lt;p&gt;&lt;img src=&#34;https://wilw.dev/media/blog/running1.jpg&#34; alt=&#34;Walking through Cardiff&#34;&gt;&lt;/p&gt;
&lt;p&gt;Now, and since last March, my daily commute simply involves me walking down the stairs to the corner of my living room that is my home “office”. Whilst it is definitely convenient (and I would certainly prefer this to full-time back in an office), it had its downsides.&lt;/p&gt;
&lt;p&gt;For the first few weeks, I just felt &lt;em&gt;lazy&lt;/em&gt;. I was working hard (we all were, and were performing great as a remote team), but my body almost craved that morning walk. The walk was time that enabled my mind to sort itself out ready for the day of work, meetings, decisions, and everything else.&lt;/p&gt;
&lt;p&gt;Without that walk time I felt my work starts were slower, and I was more easily distracted in the mornings. To try and alleviate this a little, I began walking around a park near me each evening after work - this definitely helped me wind down and the effects lasted until the following day.&lt;/p&gt;
&lt;h2 id=&#34;-workouts&#34;&gt;🏋️‍♂️ Workouts&lt;/h2&gt;
&lt;p&gt;Around the same time I began working from home full-time, my brother told me about an app - &lt;a href=&#34;https://fitbod.me&#34; target=&#34;_blank&#34; rel=&#34;noopener noreferrer&#34; &gt;Fitbod&lt;/a&gt; - that aims to be like a mini personal trainer. It’s not the only app of its type around, but it caught me at the right time.&lt;/p&gt;
&lt;p&gt;I thought that having an additional exercise goal each day - as well as the evening walk - would help in making me feel more invigorated. I began using it in the afternoons after I had finished my main work for the day (before my walk).&lt;/p&gt;
&lt;p&gt;Daily workouts, just simple ones at home following the app’s instructions, definitely had a positive effect on my mental wellbeing - it felt almost like personal meditation time for me.&lt;/p&gt;
&lt;p&gt;It wasn’t long before I switched the routine to morning workouts (before work or after my first meetings of the day). This definitely helped my work too. I’ve been doing the same thing ever since (I think I’ve only missed 10 or so days of workouts in total for the whole of the last year).&lt;/p&gt;
&lt;h2 id=&#34;-adopting-a-dog&#34;&gt;🐶 Adopting a dog&lt;/h2&gt;
&lt;p&gt;In early December we adopted a dog, and this flipped things on their head a bit. Suddenly control over my own life changed slightly, as I now had someone to be responsible for and think of - at many times before myself. I’ll write more about my dog in a later post, but will move on back to exercise for now.&lt;/p&gt;
&lt;p&gt;Since getting the dog, I no longer had times for nice leisurely walks after work or workouts in the morning. I now had a new member of the family that needed to be walked once or twice a day, and entertained during the times at home.&lt;/p&gt;
&lt;p&gt;People who tell you that you do more exercise when you have a dog are lying. When “walking” him, my time is spent mostly standing around whilst he runs and plays with his friends in the park. It’s the only way he can get real exercise - walking him on a lead on my usual walk (especially a dog with as much energy as mine!) just does not give him the exercise he needs.&lt;/p&gt;
&lt;p&gt;I wanted to find a way to maintain my level of exercise whilst also giving me the time to go to the parks for 1-2 hours each day to allow my dog to run around properly off the lead. (Note: we live in a city, and it’s not very convenient to have to drive out to countryside trails every day).&lt;/p&gt;
&lt;h2 id=&#34;-re-starting-to-run&#34;&gt;🏃‍♂️ (Re-)starting to run&lt;/h2&gt;
&lt;p&gt;Some of the people I met in my local dog-walking friend group are quite heavily into running. I used to love running in my mid-20s, and would jog 15km or so three times a week. I was put off as had been told by some people that it can have long-lasting damage on knees and other joints, and so I stopped for several years.&lt;/p&gt;
&lt;p&gt;Coincidentally I had recently been doing research about the long-term effects of running, and the results are mixed; some studies indicate what I had heard from others (about joint issues), but many talked about the benefits of building leg muscles and how this might even protect the joints. It also turns out that running properly and with good equipment (i.e. trainers) also makes a big positive difference.&lt;/p&gt;
&lt;p&gt;I thought that running could be a good replacement for my walk and some of my workout time - it burns the calories, helps maintain fitness, and has many positive psychological effects too. Especially if I could do it a few times a week.&lt;/p&gt;
&lt;p&gt;The dog-walking friends mentioned a shop nearby that could run some gait analysis with me and suggest running trainers most appropriate for me. I booked an appointment, ran the analysis, ordered the trainers, and within a week had them collected and at home.&lt;/p&gt;
&lt;h2 id=&#34;the-first-few-weeks&#34;&gt;The first few weeks&lt;/h2&gt;
&lt;p&gt;I’m now about three weeks back into running, so I thought I’d report on how it’s going.&lt;/p&gt;
&lt;p&gt;I thought I’d be much more of a mess than I actually am. I’m by no means quick (I do about 5:30 minutes per km on a good day), but I’m getting faster and definitely feel more fit. There’s certainly some muscle memory there still after all of these years.&lt;/p&gt;
&lt;p&gt;I run an average of three times per week, and go about 6km each time. I run first thing in the mornings before doing my workout, and then work. This then gives me the time I need to give the dog a chance to run around after work.&lt;/p&gt;
&lt;p&gt;On the days I don’t run in the morning, I instead go for a 30 minute walk with the dog.&lt;/p&gt;
&lt;p&gt;The routine is good (I ❤️ routine), I get the same (if not more) exercise than before, and my dog gets more running time too. It means I need to get up earlier in the morning (more about that in a future post), but I actually quite enjoy that.&lt;/p&gt;
&lt;p&gt;The main thing is that I no longer feel the &lt;em&gt;laziness&lt;/em&gt; I felt before. I start work with a good hour’s worth of solid exercise done every day, a nice cup of coffee, and much more focus.&lt;/p&gt;
</content:encoded>
    </item>
    
    <item>
      <title>Self-hosted notes and to-do lists</title>
      <link>https://wilw.dev/blog/2021/05/09/notes-todos/</link>
      <pubDate>Sun, 09 May 2021 21:31:00 +0000</pubDate>
      
      <guid>https://wilw.dev/blog/2021/05/09/notes-todos/</guid>
      
        <category>100daystooffload</category>
      
        <category>technology</category>
      
        <category>selfhost</category>
      
      
      <content:encoded>&lt;p&gt;In this post I will talk a little about how I handle my digital notes and to-do lists. In the spirit of my last post on &lt;a href=&#34;https://wilw.dev/blog/2021/05/05/data-sovereignty&#34;&gt;data sovereignty&lt;/a&gt;, the focus will be on self-hosted approaches.&lt;/p&gt;
&lt;h2 id=&#34;to-do-list-management&#34;&gt;To-do list management&lt;/h2&gt;
&lt;p&gt;It feels odd that the first task many new technical frameworks guide users through, by way of a tutorial, is a simple to-do list; yet finding great production-ready examples of such software can be challenging.&lt;/p&gt;
&lt;p&gt;It’s a pretty personal space. Although there are awesome time management processes out there (such as the &lt;a href=&#34;https://en.wikipedia.org/wiki/Pomodoro_Technique&#34; target=&#34;_blank&#34; rel=&#34;noopener noreferrer&#34; &gt;pomodoro technique&lt;/a&gt;), at the end of the day everyone is unique and what works for one person doesn’t necessarily work for others.&lt;/p&gt;
&lt;p&gt;A few years ago I got quite heavily into &lt;a href=&#34;https://todoist.com&#34; target=&#34;_blank&#34; rel=&#34;noopener noreferrer&#34; &gt;Todoist&lt;/a&gt;. It’s a very feature-rich platform with great apps across web, desktop, and mobile. It supports tagging, projects, deadlines, sub-tasks, and much more.&lt;/p&gt;
&lt;p&gt;However, it’s almost &lt;em&gt;too&lt;/em&gt; feature rich, and I find this can distract from the intended simplicity of to-do lists. Whilst it’s important to set up a process that allows you to work effectively, spending too long configuring and reconfiguring things is counter-productive.&lt;/p&gt;
&lt;p&gt;It also means that your data is held elsewhere and out of your control. A better solution might be one that you can keep local and sync or self-host.&lt;/p&gt;
&lt;p&gt;There are &lt;a href=&#34;https://github.com/awesome-selfhosted/awesome-selfhosted#task-managementto-do-lists&#34; target=&#34;_blank&#34; rel=&#34;noopener noreferrer&#34; &gt;a few examples of open-source to-do list alternatives&lt;/a&gt; that you can self-host. The one I use is &lt;a href=&#34;https://taskwarrior.org&#34; target=&#34;_blank&#34; rel=&#34;noopener noreferrer&#34; &gt;Taskwarrior&lt;/a&gt;.&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;Taskwarrior is Free and Open Source Software that manages your TODO list from the command line. It is flexible, fast, and unobtrusive. It does its job then gets out of your way. - &lt;em&gt;taskwarrior.org&lt;/em&gt;&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;I like Taskwarrior for many reasons. But mainly it’s the speed and clarity of use - it really does just “get out of your way”. Tags and projects are created automatically for you as you go, and querying feels as fast and sensible as &lt;a href=&#34;https://www.ledger-cli.org&#34; target=&#34;_blank&#34; rel=&#34;noopener noreferrer&#34; &gt;Ledger&lt;/a&gt; is for accounts.&lt;/p&gt;
&lt;p&gt;I have my terminal open all of the time anyway, and so I can quickly and at any time view my current list (by running &lt;code&gt;task&lt;/code&gt;), and see my currently in-play tasks listed right at the top.&lt;/p&gt;
&lt;p&gt;I can also query by tag (&lt;code&gt;task ls &#43;tagname&lt;/code&gt;), or to-dos for a specific project (&lt;code&gt;task ls project:projectname&lt;/code&gt;).&lt;/p&gt;
&lt;p&gt;Adding todos is also just as easy, and arguably quicker than commercial offerings like Todoist. E.g. if I wanted to add a new task to buy that gift for my friend (and tag it as “life”), I can just run &lt;code&gt;task add &#43;life Buy gift for Sam&lt;/code&gt; and then forget about the task for now. I can then check out my “life” todos (&lt;code&gt;task ls &#43;life&lt;/code&gt;) at a time when I’m out of work and have time to actually complete such tasks.&lt;/p&gt;
&lt;p&gt;I’m on a Mac, and so I just used &lt;code&gt;brew install task&lt;/code&gt; to install it. There is likely a &lt;a href=&#34;https://taskwarrior.org/download&#34; target=&#34;_blank&#34; rel=&#34;noopener noreferrer&#34; &gt;package for your own distribution&lt;/a&gt; too.&lt;/p&gt;
&lt;p&gt;In terms of self-hosting for multi-device setups, there is the &lt;a href=&#34;https://github.com/GothenburgBitFactory/taskserver&#34; target=&#34;_blank&#34; rel=&#34;noopener noreferrer&#34; &gt;Taskserver&lt;/a&gt; project from the same developers, and this is the recommended approach. For me, however, I only use Taskwarrior on one device and so I backup my tasks by simply syncing them to my Nextcloud. To do so, I just edited the relevant line in &lt;code&gt;~/.taskrc&lt;/code&gt;:&lt;/p&gt;
&lt;pre tabindex=&#34;0&#34;&gt;&lt;code&gt;...
data.location=~/Nextcloud/tasks
...
&lt;/code&gt;&lt;/pre&gt;&lt;p&gt;There is much more you can do with Taskwarrior, should you wish (including things like theming and task prioritising). I can certainly recommend taking a look through &lt;a href=&#34;https://taskwarrior.org/docs&#34; target=&#34;_blank&#34; rel=&#34;noopener noreferrer&#34; &gt;the documentation&lt;/a&gt; for more information.&lt;/p&gt;
&lt;h2 id=&#34;notes-and-notebooks&#34;&gt;Notes (and notebooks)&lt;/h2&gt;
&lt;p&gt;Sometimes you just can’t beat an old fashioned pen-and-paper notebook. The process of physically writing things down definitely seems to have a psychological effect on my ability to remember things. However, this approach isn’t really compatible with my other information-oriented habits (particularly backup paranoia and &lt;a href=&#34;https://wilw.dev/blog/2021/03/08/getting-mail&#34;&gt;minimalism&lt;/a&gt;).&lt;/p&gt;
&lt;p&gt;The same concepts around organising notes into notebooks, and keeping things logically organised, can still be applied to digital note-taking too.&lt;/p&gt;
&lt;p&gt;There are a number of free and commercial offerings that exist. &lt;a href=&#34;https://simplenote.com&#34; target=&#34;_blank&#34; rel=&#34;noopener noreferrer&#34; &gt;Simplenote&lt;/a&gt; is great (though perhaps a little &lt;em&gt;too&lt;/em&gt; simple). For Apple users, &lt;a href=&#34;https://bear.app&#34; target=&#34;_blank&#34; rel=&#34;noopener noreferrer&#34; &gt;Bear&lt;/a&gt; is also good, but potentially locks you into the Apple ecosystem.&lt;/p&gt;
&lt;p&gt;For some time I’ve used &lt;a href=&#34;https://obsidian.md&#34; target=&#34;_blank&#34; rel=&#34;noopener noreferrer&#34; &gt;Obsidian&lt;/a&gt;. I like Obsidian as it just uses your filesystem as a way of organising notes (directories are “notebooks” and each note is a simple markdown file). This approach also makes syncing over Nextcloud super easy (just set your Obsidian vault to a directory in your local Nextcloud sync folder, and away you go). There is also a mobile app that’s currently in closed beta.&lt;/p&gt;
&lt;p&gt;Recently I’ve been trying to get more into &lt;a href=&#34;https://joplinapp.org&#34; target=&#34;_blank&#34; rel=&#34;noopener noreferrer&#34; &gt;Joplin&lt;/a&gt;. I like this software because it is open-source, has a terminal interface as well as GUI ones, and has mobile apps available for note-taking on-the-go.&lt;/p&gt;
&lt;p&gt;Joplin also has &lt;a href=&#34;https://joplinapp.org/#nextcloud-synchronisation&#34; target=&#34;_blank&#34; rel=&#34;noopener noreferrer&#34; &gt;native sync-ability with Nextcloud&lt;/a&gt;, which is useful for backup and cross-device access. I find searching quick and intuitive, and the note editor uses Vim (by default, at least), which is great for easy editing.&lt;/p&gt;
&lt;p&gt;All in all, I still teeter on the edge between Obsidian and Joplin - both are great options and are worth exploring for your own use.&lt;/p&gt;
&lt;h2 id=&#34;open-to-ideas&#34;&gt;Open to ideas&lt;/h2&gt;
&lt;p&gt;I’m definitely open to other ideas for both note-taking and to-do list management. If you have any good examples of software to help with either of these then please get in touch!&lt;/p&gt;
</content:encoded>
    </item>
    
    <item>
      <title>Data Sovereignty</title>
      <link>https://wilw.dev/blog/2021/05/05/data-sovereignty/</link>
      <pubDate>Wed, 05 May 2021 19:50:00 +0000</pubDate>
      
      <guid>https://wilw.dev/blog/2021/05/05/data-sovereignty/</guid>
      
        <category>100daystooffload</category>
      
        <category>technology</category>
      
      
      <content:encoded>&lt;p&gt;The term ‘data sovereignty’ is something we hear much more about these days. Increasingly I’ve also heard it being mentioned in different contexts.&lt;/p&gt;
&lt;p&gt;We’ve seen it more in the world of enterprise SaaS; particularly in the case of UK-based public sector organisations amid post-Brexit data flow policies. More and more organisations are getting stricter in the geographic location of their users’ data. Whereas before most organisations would be happy as long as the data is stored somewhere within the EU, they would now require it to be stored onshore within the UK.&lt;/p&gt;
&lt;p&gt;They call this &lt;em&gt;data sovereignty&lt;/em&gt;. At our company we’re lucky to be agile enough to adapt and change our service offering to enable UK-only data processing and storage. However I can imagine many larger organisations might experience more inertia. Interestingly though, finding a UK-only mail provider isn’t as easy as it sounds - most such services offer “EU” or “US” servers, but stop there (potential SaaS service offering there: UK-based mail provider).&lt;/p&gt;
&lt;p&gt;The other place I’ve been hearing the term is in the indie tech and self-hosted community. In this case the data sovereignty concept relates more to data &lt;em&gt;ownership&lt;/em&gt;, where the individual maintains control over their own data, where it is stored, how it is processed, and often goes as far as to keep their own data at home (for example, self-hosted setups using home servers).&lt;/p&gt;
&lt;p&gt;I’m definitely in this camp too; whilst I don’t keep stuff stored at home, I do keep my own data - when possible - on private servers in a secure datacentre or on services I trust. Things just feel more in-control with this approach.&lt;/p&gt;
&lt;p&gt;Without data sovereignty, people are at risk of losing data they think they “own”. For example, someone &lt;a href=&#34;https://dcurt.is/apple-card-can-disable-your-icloud-account&#34; target=&#34;_blank&#34; rel=&#34;noopener noreferrer&#34; &gt;recently lost access to their iCloud data&lt;/a&gt; because of issues with an unrelated service.&lt;/p&gt;
&lt;p&gt;There’s not much more to this post. I just think it’s interesting that we’re hearing more and more of the same phrase being used in different contexts by different groups of people and organisations.&lt;/p&gt;
</content:encoded>
    </item>
    
    <item>
      <title>Go Time</title>
      <link>https://wilw.dev/blog/2021/05/04/gotime/</link>
      <pubDate>Tue, 04 May 2021 18:44:00 +0000</pubDate>
      
      <guid>https://wilw.dev/blog/2021/05/04/gotime/</guid>
      
        <category>100daystooffload</category>
      
        <category>technology</category>
      
        <category>podcast</category>
      
      
      <content:encoded>&lt;p&gt;I listen to a number of podcasts each week. One of these is &lt;a href=&#34;https://changelog.com/gotime&#34; target=&#34;_blank&#34; rel=&#34;noopener noreferrer&#34; &gt;Go Time&lt;/a&gt;.&lt;/p&gt;
&lt;p&gt;&lt;img src=&#34;https://wilw.dev/media/blog/go-time.png&#34; alt=&#34;Go Time logo&#34;&gt;&lt;/p&gt;
&lt;p&gt;The Go Time podcast releases episodes every Thursday. Its format is mostly comprised of panel discussions and interviews with founders and specialists in the community about the &lt;a href=&#34;https://golang.org&#34; target=&#34;_blank&#34; rel=&#34;noopener noreferrer&#34; &gt;Go programming language&lt;/a&gt;. Episodes are usually between 60 and 90 minutes long.&lt;/p&gt;
&lt;p&gt;I don’t program in Go a lot myself these days, though do have one or two older &lt;a href=&#34;https://wilw.dev/projects&#34;&gt;projects&lt;/a&gt; written in the language. However, I feel that the content is often broadly relevant for non-full-time gophers - like myself - also.&lt;/p&gt;
&lt;p&gt;The episodes include discussions around a diverse variety of topics - such as testing, networking, web-apps, tooling, startups, programming principles, and much more. Many of these concepts are interesting to gophers and non-gophers alike, as they touch on the broader problems as well as to dicuss how Go can specifically be used to solve these problems.&lt;/p&gt;
&lt;p&gt;Recently I have started using the &lt;a href=&#34;https://www.rust-lang.org&#34; target=&#34;_blank&#34; rel=&#34;noopener noreferrer&#34; &gt;Rust language&lt;/a&gt; more and more, and particularly on &lt;a href=&#34;https://git.wilw.dev/wilw/capsule-town&#34; target=&#34;_blank&#34; rel=&#34;noopener noreferrer&#34; &gt;this side project&lt;/a&gt; which I have used as a mechanism for learning the ins-and-outs. Although the two languages (Go and Rust) are by no means the same, they do share a small number of similar attributes and I have found that the Go Time podcast has often touched on topics relevant to both languages.&lt;/p&gt;
&lt;p&gt;Episodes also feature interesting guests from a variety of backgrounds - from specialists in the community through to startup founders. Hearing their stories is always great. Additionally, the show hosts are engaging and add light-heartedness to what can be deep technical conversations.&lt;/p&gt;
&lt;p&gt;If you’re a programmer, and even if not a gopher yourself, I recommend checking out a few of the episodes to see if you agree.&lt;/p&gt;
&lt;p&gt;It should come up in your podcast app if you search for “Go Time”. I use &lt;a href=&#34;https://overcast.fm&#34; target=&#34;_blank&#34; rel=&#34;noopener noreferrer&#34; &gt;Overcast&lt;/a&gt; on iOS, and if you do also you can subscribe at &lt;a href=&#34;https://overcast.fm/itunes1120964487/go-time&#34; target=&#34;_blank&#34; rel=&#34;noopener noreferrer&#34; &gt;this link&lt;/a&gt;.&lt;/p&gt;
</content:encoded>
    </item>
    
    <item>
      <title>Starting out with the Pinephone</title>
      <link>https://wilw.dev/blog/2021/04/27/pinephone/</link>
      <pubDate>Tue, 27 Apr 2021 21:00:00 +0000</pubDate>
      
      <guid>https://wilw.dev/blog/2021/04/27/pinephone/</guid>
      
        <category>100daystooffload</category>
      
        <category>technology</category>
      
        <category>pinephone</category>
      
      
      <content:encoded>&lt;p&gt;As you may know, I &lt;a href=&#34;https://wilw.dev/blog/2021/03/27/pinephone-pinetime&#34;&gt;recently purchased the beta edition of the Pinephone&lt;/a&gt;. It arrived last week in the &lt;em&gt;Pinephone Beta Edition&lt;/em&gt; box shown below.&lt;/p&gt;
&lt;p&gt;&lt;img src=&#34;https://wilw.dev/media/blog/pinephone.jpg&#34; alt=&#34;Pinephone beta box&#34;&gt;&lt;/p&gt;
&lt;p&gt;As mentioned in my previous post on the subject, I bought the phone for purely experimental purposes, to get involved in the community, and to be a part of the freedom and Linux-on-phone movement.&lt;/p&gt;
&lt;p&gt;I fully understand that the device is not yet really considered ready for every-day reliable production use (especially when compared to my current iPhone 11 Pro Max). However, &lt;a href=&#34;https://wiki.pine64.org/index.php/PinePhone&#34; target=&#34;_blank&#34; rel=&#34;noopener noreferrer&#34; &gt;the Pinephone&lt;/a&gt; is less than 20% the price of my iPhone, and comes with the freedom to do so much more - without the restrictions of Apple’s “walled garden”.&lt;/p&gt;
&lt;p&gt;However, I am very excited to see what &lt;em&gt;can&lt;/em&gt; be done with it. At the end of the day, it’s just an ARM-based &lt;em&gt;computer&lt;/em&gt; with support for running mainline Linux and the added benefit of having cellular capabilities to make phone calls and handle data connections.&lt;/p&gt;
&lt;p&gt;It’s also super easy to try out different &lt;a href=&#34;https://wiki.pine64.org/wiki/PinePhone_Software_Releases&#34; target=&#34;_blank&#34; rel=&#34;noopener noreferrer&#34; &gt;operating systems&lt;/a&gt; by simply &lt;code&gt;dd&lt;/code&gt;-ing to an SD card - much easier than the tedious root-recovery-flash song and dance often required in the Android ecosystem.&lt;/p&gt;
&lt;h1 id=&#34;the-next-few-weeks&#34;&gt;The next few weeks&lt;/h1&gt;
&lt;p&gt;Anyway, I’m going a little off-topic. My initial plans aren’t to try out new operating systems just yet (although I am excited to try). Instead, I’d like to spend the first few weeks tinkering with the out-of-the (beta) box underlying system and seeing how well it &lt;em&gt;does&lt;/em&gt; handle my day-to-day tasks on an as-is (i.e. without needing to change SD card) basis.&lt;/p&gt;
&lt;p&gt;The beta edition comes pre-installed with &lt;a href=&#34;https://manjaro.org&#34; target=&#34;_blank&#34; rel=&#34;noopener noreferrer&#34; &gt;Manjaro Linux&lt;/a&gt; on the eMMC along with the &lt;a href=&#34;https://www.plasma-mobile.org&#34; target=&#34;_blank&#34; rel=&#34;noopener noreferrer&#34; &gt;KDE Plasma Mobile&lt;/a&gt; desktop environment, so this is what I’ll stick with for now. Upon initial boot-up I can already see that it comes pre-installed with some useful packages (e.g. Telegram messenger and the &lt;a href=&#34;https://git.sr.ht/~martijnbraam/megapixels&#34; target=&#34;_blank&#34; rel=&#34;noopener noreferrer&#34; &gt;Megapixels camera application&lt;/a&gt;).&lt;/p&gt;
&lt;p&gt;However, below is a list of day-to-day tasks I can do on my current phone and which I will try and accomplish using the device over the next few weeks.&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;Basic calls and texts.&lt;/li&gt;
&lt;li&gt;4G cellular data connectivity.&lt;/li&gt;
&lt;li&gt;WiFi connectivity.&lt;/li&gt;
&lt;li&gt;Bluetooth connectivity (including headphones).&lt;/li&gt;
&lt;li&gt;Photo- and video-taking using both front- and rear-facing cameras.&lt;/li&gt;
&lt;li&gt;Web browsing.&lt;/li&gt;
&lt;li&gt;Podcast subscribing, listing, and listening.&lt;/li&gt;
&lt;li&gt;Audiobook downloading and listening.&lt;/li&gt;
&lt;li&gt;Music-playing (preferably through Spotify).&lt;/li&gt;
&lt;li&gt;Mastodon (tooting and reading my timelines).&lt;/li&gt;
&lt;li&gt;Twitter.&lt;/li&gt;
&lt;li&gt;RSS (viewing my feeds from my FreshRSS server).&lt;/li&gt;
&lt;li&gt;Email reading and sending.&lt;/li&gt;
&lt;li&gt;Telegram messaging.&lt;/li&gt;
&lt;li&gt;Password-management.&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;I’ve purposefully kept a couple of things off this list - including Whatsapp, my bank’s app, and some enterprise apps I use for work - since these systems are proprietary in nature and so would not be fair to expect of the phone. One could argue that this impacts its viability as a daily-driver, however that is not my current goal. Presently I am just looking to see how well some basic tasks can be accomplished before trying to take it further to be fully useful for daily use.&lt;/p&gt;
&lt;p&gt;I also want to document the journey for myself and others wanting to get involved in this project.&lt;/p&gt;
&lt;p&gt;Projects like &lt;a href=&#34;https://linmob.net/2020/08/15/anbox-on-the-pinephone.html&#34; target=&#34;_blank&#34; rel=&#34;noopener noreferrer&#34; &gt;Anbox&lt;/a&gt; look like potential routes for getting additional things working when needed in a pinch. However I’ll save that for another time.&lt;/p&gt;
&lt;h1 id=&#34;next&#34;&gt;Next&lt;/h1&gt;
&lt;p&gt;Check back in a few weeks to see how I get on. If you have any advice for starting out in this way then please let me know!&lt;/p&gt;
&lt;p&gt;After this initial period I will look to try out other shells and underlying systems. The &lt;a href=&#34;https://github.com/dreemurrs-embedded/Pine64-Arch&#34; target=&#34;_blank&#34; rel=&#34;noopener noreferrer&#34; &gt;ARM Arch with Phosh project&lt;/a&gt; looks like a good start point for when I come to this.&lt;/p&gt;
</content:encoded>
    </item>
    
    <item>
      <title>My appearance in the Wales &#34;35 Under 35&#34;</title>
      <link>https://wilw.dev/blog/2021/04/26/35-under-35/</link>
      <pubDate>Mon, 26 Apr 2021 10:48:00 +0000</pubDate>
      
      <guid>https://wilw.dev/blog/2021/04/26/35-under-35/</guid>
      
        <category>100daystooffload</category>
      
        <category>life</category>
      
      
      <content:encoded>&lt;p&gt;This is a bit of a vanity post, but back in December I was lucky enough to be included in the 2020 &lt;a href=&#34;https://www.walesonline.co.uk/news/wales-news/walesonline-35-under-35-top-19351410&#34; target=&#34;_blank&#34; rel=&#34;noopener noreferrer&#34; &gt;WalesOnline “35 Under 35”&lt;/a&gt;.&lt;/p&gt;
&lt;p&gt;This list aims to present the “best young businessmen in Wales” for the year. It was definitely an honour to be included and it’s great to see the efforts from the whole team at &lt;a href=&#34;https://www.simplydo.co.uk&#34; target=&#34;_blank&#34; rel=&#34;noopener noreferrer&#34; &gt;Simply Do&lt;/a&gt; reflected. We’re still only at the beginning of our journey and so we have an exciting few years ahead!&lt;/p&gt;
</content:encoded>
    </item>
    
    <item>
      <title>Steve Jobs by Walter Isaacson</title>
      <link>https://wilw.dev/blog/2021/04/25/steve-jobs/</link>
      <pubDate>Sun, 25 Apr 2021 16:41:00 +0000</pubDate>
      
      <guid>https://wilw.dev/blog/2021/04/25/steve-jobs/</guid>
      
        <category>100daystooffload</category>
      
        <category>book</category>
      
      
      <content:encoded>&lt;p&gt;I was recently asked whether Steve Jobs was someone that inspired me. It’s a difficult question, I find; he’s definitely an inspiring person in the sense of his work ethic, the products he envisages, and his way of understanding the needs of the target customer better than they know it themselves.&lt;/p&gt;
&lt;p&gt;As a person, however, I find his personality and the way he treats others less inspiring. I try to be empathetic to others and take into account the emotional and psychological position of someone else when interacting with them. In a professional workplace this (hopefully) contributes towards creating a space that enables people to grow and develop whilst also emboldening colleagues to put forward their own thoughts and opinions in a more risk-free environment.&lt;/p&gt;
&lt;p&gt;Jobs, on the other hand, has his own vision and - although these visions, if executed, are bound to be successful - you’ll need to be on &lt;em&gt;his&lt;/em&gt; train in order to succeed in working with him.&lt;/p&gt;
&lt;p&gt;&lt;img src=&#34;https://wilw.dev/media/blog/steve_jobs.jpg&#34; alt=&#34;Steve Jobs biography book cover&#34;&gt;&lt;/p&gt;
&lt;p&gt;The reason my colleague asked me this question was because I was reading the &lt;a href=&#34;https://www.goodreads.com/book/show/11084145-steve-jobs&#34; target=&#34;_blank&#34; rel=&#34;noopener noreferrer&#34; &gt;Steve Jobs biography&lt;/a&gt; by &lt;a href=&#34;https://www.goodreads.com/author/show/7111.Walter_Isaacson&#34; target=&#34;_blank&#34; rel=&#34;noopener noreferrer&#34; &gt;Walter Isaacson&lt;/a&gt; at the time. The biography’s subject is not a hero of mine in any way, but he is indisputably a legend in the consumer technology space and so his story definitely deserves knowing (whatever your particular stance is).&lt;/p&gt;
&lt;p&gt;Although I knew the rough story of his life - his co-founding of Apple with Steve Wozniak, his time and successes at Pixar, his founding of NeXT before his subsequent return to Apple and eventual battle with cancer - understanding how individual products came to be imagined and created was fascinating.&lt;/p&gt;
&lt;p&gt;His relationships with others - friends, colleagues, competitors, and romances - undoubtedly helped shape his life and his successes. His obsessions over food, art (and the appearance of products, both outside and within) and his focus on work right to the end were certainly areas I did not know about, but it’s clear that these all contribute towards what he managed to achieve.&lt;/p&gt;
&lt;p&gt;I know that a lot of people don’t like Jobs, or don’t agree with the type of closed end-to-end technology he pioneered and obsessed over (myself included), however his achievements - even by the age of 30 - and his focus on the end goal should definitely be an inspiration to all technologists.&lt;/p&gt;
</content:encoded>
    </item>
    
    <item>
      <title>The Giver of Stars by Jojo Moyes</title>
      <link>https://wilw.dev/blog/2021/04/18/giver-of-stars/</link>
      <pubDate>Sun, 18 Apr 2021 12:09:00 +0000</pubDate>
      
      <guid>https://wilw.dev/blog/2021/04/18/giver-of-stars/</guid>
      
        <category>100daystooffload</category>
      
        <category>book</category>
      
      
      <content:encoded>&lt;p&gt;&lt;a href=&#34;https://www.goodreads.com/book/show/43925876-the-giver-of-stars&#34; target=&#34;_blank&#34; rel=&#34;noopener noreferrer&#34; &gt;The Giver of Stars&lt;/a&gt; by &lt;a href=&#34;https://www.goodreads.com/author/show/281810.Jojo_Moyes&#34; target=&#34;_blank&#34; rel=&#34;noopener noreferrer&#34; &gt;Jojo Moyes&lt;/a&gt; tells the story of a young English woman - Alice - who marries an American man and moves to a small town in Kentucky in the late 1930s.&lt;/p&gt;
&lt;p&gt;&lt;img src=&#34;https://wilw.dev/media/blog/giver_of_stars.jpg&#34; alt=&#34;The Giver of Stars book cover&#34;&gt;&lt;/p&gt;
&lt;p&gt;Not long after arriving in Kentucky Alice realises she may have made a mistake when it comes to her new husband. However, the real story focuses around a job Alice gets working with the local library.&lt;/p&gt;
&lt;p&gt;The library begins offering a new service, in which the (female) librarians travel around the local area (often hard to traverse due to mountainous terrain) on horseback to deliver books to those unable to get to town or who wouldn’t usually engage with the library. The concept is based on a real project - the &lt;a href=&#34;https://en.wikipedia.org/wiki/Pack_Horse_Library_Project&#34; target=&#34;_blank&#34; rel=&#34;noopener noreferrer&#34; &gt;Pack Horse Library Project&lt;/a&gt; - and Alice and the other women are met with many different types of personalities on their rounds.&lt;/p&gt;
&lt;p&gt;There are focuses on racism, sexism, misogyny, domestic abuse, murder, and much more in the story, and the librarians are faced with a number of hugely difficult situations both when at work and when at home.&lt;/p&gt;
&lt;p&gt;The story was fantastic and engaging. I enjoyed the scene-setting, and could easily picture the local town and all the surrounding countryside. You feel an undeniable sense of unfairness in the world as the story progresses - in which rich white men nearly always get their own way in most situations - however the bond that builds between the characters, and their shared experiences, show that this can be overcome.&lt;/p&gt;
</content:encoded>
    </item>
    
    <item>
      <title>Reporting business accounts using Ledger</title>
      <link>https://wilw.dev/blog/2021/04/17/business-accounts-ledger/</link>
      <pubDate>Sat, 17 Apr 2021 14:00:00 +0000</pubDate>
      
      <guid>https://wilw.dev/blog/2021/04/17/business-accounts-ledger/</guid>
      
        <category>100daystooffload</category>
      
        <category>finance</category>
      
        <category>technology</category>
      
        <category>ledger</category>
      
      
      <content:encoded>&lt;p&gt;As is the case with many countries, all businesses in the UK must report the state of their financial accounts to the relevant inland revenue service at their year-end (in the UK, this is &lt;a href=&#34;https://www.gov.uk/government/organisations/hm-revenue-customs&#34; target=&#34;_blank&#34; rel=&#34;noopener noreferrer&#34; &gt;HMRC&lt;/a&gt;).&lt;/p&gt;
&lt;p&gt;This is also the case if you are a freelancer or sole-trader (or if you’ve made other untaxed income - e.g. from investments). In these cases, this is called your &lt;a href=&#34;https://www.gov.uk/self-assessment-tax-returns&#34; target=&#34;_blank&#34; rel=&#34;noopener noreferrer&#34; &gt;Self Assessment&lt;/a&gt;. Self Assessments are pretty straight forward, and can usually be completed online by the indiviual themself - as long as they have kept good accounts and know their numbers.&lt;/p&gt;
&lt;p&gt;However, the required year-end &lt;em&gt;business&lt;/em&gt; accounts are different and are more complex in order to account for all the different types of operating models and variety of business types. There are also various rules for businesses of different sizes and if you don’t know what you’re doing you may end up paying too much or too little tax.&lt;/p&gt;
&lt;p&gt;As such, it is generally advisable to appoint an accountant to help you at your year-end (even if you’re making a loss!). It gives you peace of mind and also saves you time.&lt;/p&gt;
&lt;p&gt;My business year-end passed recently. Historically I’ve used &lt;a href=&#34;https://xero.com&#34; target=&#34;_blank&#34; rel=&#34;noopener noreferrer&#34; &gt;Xero&lt;/a&gt; to track business finances - since it is a good one-stop shop for issuing invoices, getting paid, tracking bills and finances, and automatically reconciling against your bank. It’s a great tool for small businesses as it helps you make sure everything is correctly accounted for, and it allows your accountant to easily get the information they need in order to make their reports for your business to HMRC.&lt;/p&gt;
&lt;p&gt;However, it is a paid-for service, and if you’ve paused trading at least temporarily - like me - or if you’re going through a financial dry patch, it feels a waste to pay for something that you’re not using.&lt;/p&gt;
&lt;p&gt;About a year ago I got quite heavily into &lt;a href=&#34;https://wilw.dev/notes/plain-text-accounting&#34;&gt;plain-text accounting&lt;/a&gt; - it feels logical and in-control. I was using it for some of my personal finances and so I thought I’d also switch business bookkeeping to the &lt;a href=&#34;https://www.ledger-cli.org&#34; target=&#34;_blank&#34; rel=&#34;noopener noreferrer&#34; &gt;Ledger&lt;/a&gt; approach too.&lt;/p&gt;
&lt;p&gt;I exported my Xero accounts into a new Ledger file and paused my Xero subscription. Every month I would run through my bank statement/invoices/bills, and update the ledger and reconcile against the business bank account. As such, when it came round to year-end, I had a full set of books for the relevant tax period.&lt;/p&gt;
&lt;p&gt;This is where I worried a little. The lady who normally files my accounts had access to my Xero and can run everything from there (many small business accountants in the UK recommend and sometimes only work with Xero). I didn’t want to have to look for and begin working with a new accountant, and so I looked to see if I could get Ledger to output balance sheets and P&amp;Ls in a similar way to Xero.&lt;/p&gt;
&lt;p&gt;The Ledger tool offers a number of reporting mechanisms. The most useful are perhaps the &lt;code&gt;balance&lt;/code&gt; and &lt;code&gt;register&lt;/code&gt; commands, which respectively show the balance across your accounts and a transaction log.&lt;/p&gt;
&lt;p&gt;After running a few of these simple Ledger commands, I had the files I needed: a balance sheet (covering all accounts), a profit &amp; loss account (essentially a balance sheet covering income and expense accounts), and a transaction register. Examples describing how I generated these are shown below (in this case assuming a year-end of 31st December).&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Balance sheet:&lt;/strong&gt; To generate the balance sheet I used &lt;code&gt;ledger balance -b 2020/01/01 -e 2021/01/01&lt;/code&gt;, which outputs something along the lines of:&lt;/p&gt;
&lt;pre tabindex=&#34;0&#34;&gt;&lt;code&gt;          £-XXXX.XX   Assets
           £XXXX.XX    Bank 1
          £-XXXX.XX    Bank 2
           £XXXX.XX   Equity:Shareholder:Dividends
           £XXXX.XX   Expenses
             £XXX.XX    Advertising
              £XX.XX    Compliance
              £XX.XX    Domains
             £XXX.XX    Hosting
             £XXX.XX    Services
             £XXX.XX      Accounting
               £X.XX      Banking
              £XX.XX      Legal
             £XXX.XX    Software
            £XXXX.XX    Tax:Corporation
            £-XXX.XX  Income:Sales:Product
--------------------
                   0
&lt;/code&gt;&lt;/pre&gt;&lt;p&gt;&lt;strong&gt;Profit &amp; loss account:&lt;/strong&gt; The rough “P&amp;L” was generated with &lt;code&gt;ledger balance -b 2020/01/01 -e 2021/01/01 income expenses&lt;/code&gt;:&lt;/p&gt;
&lt;pre tabindex=&#34;0&#34;&gt;&lt;code&gt;           £XXXX.XX   Expenses
             £XXX.XX    Advertising
              £XX.XX    Compliance
              £XX.XX    Domains
             £XXX.XX    Hosting
             £XXX.XX    Services
             £XXX.XX      Accounting
               £X.XX      Banking
              £XX.XX      Legal
             £XXX.XX    Software
            £XXXX.XX    Tax:Corporation
            £-XXX.XX  Income:Sales:Product
--------------------
            £XXXX.XX
&lt;/code&gt;&lt;/pre&gt;&lt;p&gt;(Where the final line indicates the overall balance between income and expenses).&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Transaction log:&lt;/strong&gt; The register was generated using &lt;code&gt;ledger register -b 2020/01/01 -e 2021/01/01&lt;/code&gt;. I won’t include a sample below, as a transaction log is mostly obvious. I also generated it in CSV format in case this made it easier for the accountant at all: &lt;code&gt;ledger csv -b 2020/01/01 -e 2021/01/01&lt;/code&gt;.&lt;/p&gt;
&lt;p&gt;I placed the outputs from these commands into separate files and sent them to the accountant, who was then able to submit the company accounts without needing Xero. This was a great experience, as it gives me confidence in the end-to-end functionality of Ledger (and other similar command-line accounting tools). Writing and keeping books using plain-text files is quicker than Xero (which can be quite clunky), and now I can also easily get the information out the other end too. And it’s free!&lt;/p&gt;
</content:encoded>
    </item>
    
    <item>
      <title>Six months of Invisalign</title>
      <link>https://wilw.dev/blog/2021/04/12/invisalign/</link>
      <pubDate>Mon, 12 Apr 2021 21:38:00 +0000</pubDate>
      
      <guid>https://wilw.dev/blog/2021/04/12/invisalign/</guid>
      
        <category>100daystooffload</category>
      
        <category>life</category>
      
      
      <content:encoded>&lt;p&gt;Back in November I started an &lt;a href=&#34;https://www.invisalign.com&#34; target=&#34;_blank&#34; rel=&#34;noopener noreferrer&#34; &gt;Invisalign&lt;/a&gt; course to help straighten my teeth. Invisalign works like traditional braces, but is instead formed from transparent teeth “trays” that others can only really notice up-close. Given my personal situation, this seemed like a better approach than the traditional metal braces.&lt;/p&gt;
&lt;p&gt;&lt;img src=&#34;https://wilw.dev/media/blog/invisalign.png&#34; alt=&#34;My Invisalign goodie bags&#34;&gt;&lt;/p&gt;
&lt;p&gt;In all honesty, my teeth weren’t that bad to begin with but - like many people - as I got older I was beginning to notice a little more “crowding” (where teeth bunch together and start to move out of place). Invisalign was something I had wanted to try for a while, and whilst the UK was in lockdown and I couldn’t see anyone anyway, it felt like a good time to go ahead with it.&lt;/p&gt;
&lt;h1 id=&#34;the-process&#34;&gt;The process&lt;/h1&gt;
&lt;p&gt;I had a couple of initial appointments with my dentist just to ensure I was dentally fit for orthodontic work and in order to take a scan of my teeth. The scan was cool - it showed exactly what my teeth looked like and the software then uses the result to design a series of aligners that would bring the teeth back into line. I also got access to a website on which I could see how my teeth would be moving over time.&lt;/p&gt;
&lt;p&gt;After my scans, I went back to the dentist a couple of weeks later in order for some attachments to be added to my teeth and to collect my newly-manufactured aligners. In total I was given 22 sets of aligners, with the aim being to start with set number 1 and then proceed to the next one each week - every change in aligner gradually moving the teeth into line.&lt;/p&gt;
&lt;p&gt;I was also given a scanbox, into which I could place my phone in order to submit photos of my teeth every week through an app to my dentist. This enabled him to track the progress each week and to ensure I moved onto the next aligner set at the right time.&lt;/p&gt;
&lt;p&gt;For the next two-three months I wore my aligners for 22 hours each day. Every week, I scanned my teeth and was instructed to move onto the next set of aligners in order to progress the treatment. In February I had to go back to visit the dentist in order to have some additional filing between some of my teeth so they could move into position properly.&lt;/p&gt;
&lt;p&gt;I then continued for another few months - until today. I completed my last set of aligners last week and had a check-up this afternoon to see how things went. I was pleased with the result, and we agreed that no more movement was needed. My dentist removed the attachments from my teeth and we ordered the retainers, which I will need to continue to wear full-time for a few months and then beyond that just at night - in order to ensure things stay in place.&lt;/p&gt;
&lt;h1 id=&#34;my-thoughts&#34;&gt;My thoughts&lt;/h1&gt;
&lt;p&gt;In general, the process was super easy. For the first few days of the treatment I didn’t think I would be able to keep it up for six months - the aligners felt pretty uncomfortable and can be a little painful for a couple of days every time I switched to a new set. Also, the extra work needed when brushing my teeth and having to remove the aligners between meals seemed inconvenient.&lt;/p&gt;
&lt;p&gt;However, after a few weeks it all became second nature. It now feels weird when I don’t have them in!&lt;/p&gt;
&lt;p&gt;The treatment is also quite expensive. However, it is cheaper (I think?) than traditional braces, it’s a shorter treatment period, and I preferred to have the almost-invisible aligners rather than metal braces in front of my teeth.&lt;/p&gt;
&lt;p&gt;In addition, given that the sets of post-treatment retainers included in the treatment plan last for years, it feels like the treatment is a “one off” (🤞) - as long as I keep wearing the retainers properly then the teeth should now stay in place.&lt;/p&gt;
&lt;p&gt;All in all, it was (and is still) a good experience and I am glad to have done it.&lt;/p&gt;
</content:encoded>
    </item>
    
    <item>
      <title>Is Facebook scraping the Fediverse?</title>
      <link>https://wilw.dev/blog/2021/04/07/is-facebook-scraping-fediverse/</link>
      <pubDate>Wed, 07 Apr 2021 19:44:00 +0000</pubDate>
      
      <guid>https://wilw.dev/blog/2021/04/07/is-facebook-scraping-fediverse/</guid>
      
        <category>100daystooffload</category>
      
        <category>technology</category>
      
      
      <content:encoded>&lt;p&gt;I don’t use Facebook often. In fact, I only have an account currently because our company uses the “Login with Facebook” functionality in order to offer an additional single sign-on option for some customers.&lt;/p&gt;
&lt;p&gt;I logged-in today as we needed to update some of the app’s configuration on the Facebook Developer portal, and I went via the Facebook homepage feed to get there. A couple of “Suggested for you” posts that showed near the top of my feed were unusual and caught my eye.&lt;/p&gt;
&lt;p&gt;&lt;img src=&#34;https://wilw.dev/media/blog/facebook_tram_1.png&#34; alt=&#34;Facebook Suggested Post Tram picture&#34;&gt;&lt;/p&gt;
&lt;p&gt;There wasn’t just one. As I scrolled further, more and more showed up - all seemingly from the same user.&lt;/p&gt;
&lt;p&gt;&lt;img src=&#34;https://wilw.dev/media/blog/facebook_tram_2.png&#34; alt=&#34;Another Facebook Suggested Post Tram picture&#34;&gt;&lt;/p&gt;
&lt;p&gt;&lt;img src=&#34;https://wilw.dev/media/blog/facebook_tram_3.png&#34; alt=&#34;Yet another Facebook Suggested Post Tram picture&#34;&gt;&lt;/p&gt;
&lt;p&gt;The page (“Nostalgia Vienna”) doesn’t seem to be selling anything in these posts, and I’ve never interacted with them before. I also don’t have any content on Facebook and use browser plugins such as &lt;a href=&#34;https://addons.mozilla.org/en-US/firefox/addon/multi-account-containers&#34; target=&#34;_blank&#34; rel=&#34;noopener noreferrer&#34; &gt;Firefox Containers&lt;/a&gt;, &lt;a href=&#34;https://addons.mozilla.org/en-US/firefox/addon/privacy-badger17&#34; target=&#34;_blank&#34; rel=&#34;noopener noreferrer&#34; &gt;Privacy Badger&lt;/a&gt;, and others to try and prevent inadvertent data sharing with the social platform.&lt;/p&gt;
&lt;p&gt;I know Facebook potentially has other ways of gathering user information, but I just simply don’t have a big interest in Viennese trams (or trams in general). I don’t really know why it is so keen to show me a new picture of a tram every few posts down the home feed.&lt;/p&gt;
&lt;p&gt;I then realised that I recently &lt;a href=&#34;https://pixelfed.social/p/wilw/267598377078886400&#34; target=&#34;_blank&#34; rel=&#34;noopener noreferrer&#34; &gt;posted a picture to Pixelfed&lt;/a&gt; of a tram that I took on a trip to Basel a few years back.&lt;/p&gt;
&lt;p&gt;&lt;img src=&#34;https://wilw.dev/media/blog/pixelfed_tram.png&#34; alt=&#34;A screenshot of my tram photo from Pixelfed&#34;&gt;&lt;/p&gt;
&lt;p&gt;My &lt;a href=&#34;https://pixelfed.social/wilw&#34; target=&#34;_blank&#34; rel=&#34;noopener noreferrer&#34; &gt;Pixelfed account&lt;/a&gt; is not explicitly tied to my own name or identity, but my bio there does contain a link to &lt;a href=&#34;https://wilw.dev&#34; target=&#34;_blank&#34; rel=&#34;noopener noreferrer&#34; &gt;my website&lt;/a&gt;.&lt;/p&gt;
&lt;p&gt;Interestingly, the styles and images of the Viennese trams suggested by Facebook are not a million miles away from my own post of the Swiss tram. The link feels tenuous but I can’t think of anything else that might cause Facebook’s algorithm to so strongly suggest this type of content to me.&lt;/p&gt;
&lt;p&gt;I just wonder whether there is some clever scraping going on behind the scenes to further bolster Facebook’s knowledge of its users.&lt;/p&gt;
</content:encoded>
    </item>
    
    <item>
      <title>From Apple Mail to Spark to Thunderbird</title>
      <link>https://wilw.dev/blog/2021/04/04/applemail-spark-thunderbird/</link>
      <pubDate>Sun, 04 Apr 2021 11:10:00 +0000</pubDate>
      
      <guid>https://wilw.dev/blog/2021/04/04/applemail-spark-thunderbird/</guid>
      
        <category>100daystooffload</category>
      
        <category>technology</category>
      
        <category>opinion</category>
      
      
      <content:encoded>&lt;p&gt;Like many people, I own and manage multiple email accounts - for example, some are for work, for home, or for specific projects. I used to be a strong user of solely web-based email clients (such as Gmail or Fastmail’s web apps) for each of my accounts. However the number of tabs I needed to keep open for all of this grew to the point where things became unmanageable - both in terms of needing to check multiple tabs several times per day and also frustrations when the browser would restart, or if I’d lose my tab setup for some other reason.&lt;/p&gt;
&lt;p&gt;I needed a proper client, and although I knew that web-based software like &lt;a href=&#34;https://roundcube.net&#34; target=&#34;_blank&#34; rel=&#34;noopener noreferrer&#34; &gt;Roundcube&lt;/a&gt; and &lt;a href=&#34;https://www.rainloop.net&#34; target=&#34;_blank&#34; rel=&#34;noopener noreferrer&#34; &gt;Rainloop&lt;/a&gt; existed - which I could self-host - they just never felt stable or feature-ful enough.&lt;/p&gt;
&lt;p&gt;This post is a short round-up of three mail clients I’ve been trying over the past few months.&lt;/p&gt;
&lt;h2 id=&#34;apple-mail&#34;&gt;Apple Mail&lt;/h2&gt;
&lt;p&gt;For several years I’ve been an Apple Mail user on both my Mac and iPhone - mainly because it was the default on the devices I have but also because it’s generally quite smooth and works reliably.&lt;/p&gt;
&lt;p&gt;It’s relatively painless to get setup, and the accounts sync well across the mail, contacts, and calendar apps. On Gmail (and other larger providers) there is an authentication “wizard” to help get the accounts setup. Fastmail allows you to &lt;a href=&#34;https://www.fastmail.help/hc/en-us/articles/1500000279941-Set-up-iOS-devices-iOS-12&#34; target=&#34;_blank&#34; rel=&#34;noopener noreferrer&#34; &gt;install profiles&lt;/a&gt; that automatically configure everything for you.&lt;/p&gt;
&lt;p&gt;However, over time I began to find the interface a bit unintuitive. On iOS the general “Accounts” setting - which was useful as a single source of truth - seemed to disappear, and for some mailboxes it wouldn’t let me add alias send-from addresses. I’m sure there was a reason for this, but I sometimes find that the iOS settings UIs overcomplicate things in their efforts for simplicity.&lt;/p&gt;
&lt;p&gt;Whilst the Mac (currently) still has a dedicated Accounts setting in System Preferences, it also had other problems. Several times a day I’d frustratingly get my workflow interrupted by warnings of network problems.&lt;/p&gt;
&lt;p&gt;&lt;img src=&#34;https://wilw.dev/media/blog/apple-mail-issue.png&#34; alt=&#34;Apple Mail &amp;lsquo;accounts offline&amp;rsquo; warning&#34;&gt;&lt;/p&gt;
&lt;p&gt;I still think Apple Mail is a pretty decent app, but I thought that there must be something else out there that would work better, and be less frustrating, for me.&lt;/p&gt;
&lt;h2 id=&#34;spark-mail&#34;&gt;Spark Mail&lt;/h2&gt;
&lt;p&gt;Back in February some of my colleagues recommended the &lt;a href=&#34;https://sparkmailapp.com&#34; target=&#34;_blank&#34; rel=&#34;noopener noreferrer&#34; &gt;Spark mail app&lt;/a&gt; from &lt;a href=&#34;https://readdle.com&#34; target=&#34;_blank&#34; rel=&#34;noopener noreferrer&#34; &gt;Readdle&lt;/a&gt;. I’ve used some of Readdle’s other software in the past (see &lt;a href=&#34;https://wilw.dev/blog/2021/03/08/getting-mail&#34;&gt;this post&lt;/a&gt;, for example), and generally find it quite useful and intuitive. Like Apple Mail, it’s also available for Mac and iOS.&lt;/p&gt;
&lt;p&gt;Spark is free to get started (and I imagine most individuals would fit into their &lt;a href=&#34;https://sparkmailapp.com/pricing&#34; target=&#34;_blank&#34; rel=&#34;noopener noreferrer&#34; &gt;free plan&lt;/a&gt; long-term too). One of the features I immediately liked was that all of your mail accounts are tied to a single account. That means that if you get a new computer or phone, you don’t need to go through the tedious business of setting up all the mail accounts again - just login with your main email and everything else gets pulled-through.&lt;/p&gt;
&lt;p&gt;Email management is easy, search is lightning-fast, and the settings are useful.&lt;/p&gt;
&lt;p&gt;Spark also comes bundled with a calendar that syncs well and automatically with services like Google Calendar and Fastmail Calendar. Like Apple Mail, there are dedicated setup wizards for email and calendar with the larger providers, and an option for manual entry for others. The calendar’s event creator is nice, and also allows you to automatically schedule a video meeting.&lt;/p&gt;
&lt;p&gt;&lt;img src=&#34;https://wilw.dev/media/blog/spark-calendar-meeting.png&#34; alt=&#34;Spark calendar video meeting picker&#34;&gt;&lt;/p&gt;
&lt;p&gt;One drawback is that there doesn’t seem to be any way to view or manage contacts, and neither does it seem to integrate with the system contacts. I imagine it works directly with the relevant provider’s contacts service.&lt;/p&gt;
&lt;p&gt;Another frustration I had was in managing shared calendars. I think I’m a bit of a calendar power-user, however I imagine this must be affecting other people too. If someone else - who also shares their calendar with you - creates an event and invites you to it, there does not seem to be any way to select your own entry in order to interact with it (e.g. to accept or decline the invitation).&lt;/p&gt;
&lt;p&gt;In the event below, if my calendar was the “green” one, for example, there is no way for me to select that in order to accept or decline. Again, I may be missing something but I’ve been trying to find a way for a while now without needing to “hide” my colleagues’ calendars first.&lt;/p&gt;
&lt;p&gt;&lt;img src=&#34;https://wilw.dev/media/blog/spark-calendar-selection.png&#34; alt=&#34;Spark calendar event with multiple attendees&#34;&gt;&lt;/p&gt;
&lt;p&gt;Then it comes to security. Whilst I “trust” Readdle - in that I imagine they have decent security practices in place - we know that even the most secure companies can become compromised. The account sync feature mentioned earlier is certainly useful, however this must mean that Readdle are storing the Gmail access keys or IMAP connection details on their own servers in a centralised location. Your email is the last thing you want to get compromised - since it is likely that this controls a number of your other online accounts - and so this risk is a bit of a concern.&lt;/p&gt;
&lt;p&gt;Readdle &lt;a href=&#34;https://sparkmailapp.com/blog/privacy-explained&#34; target=&#34;_blank&#34; rel=&#34;noopener noreferrer&#34; &gt;claim that&lt;/a&gt; everything is encrypted at various levels but it still feels a little risky to me. Having the sync and push notifications is useful, and so it’s up to the individual to choose what works best for them.&lt;/p&gt;
&lt;h2 id=&#34;thunderbird&#34;&gt;Thunderbird&lt;/h2&gt;
&lt;p&gt;The last client I want to mention in this post is &lt;a href=&#34;https://www.thunderbird.net&#34; target=&#34;_blank&#34; rel=&#34;noopener noreferrer&#34; &gt;Mozilla’s Thunderbird&lt;/a&gt;. This is a bit of a re-visit for me, since this is the client I used consistently during my University years.&lt;/p&gt;
&lt;p&gt;In honesty, the client doesn’t seem to have changed a huge amount over the last decade, but then again - neither have the underlying email technologies themselves. It’s an open-source client available on a number of operating systems - but not yet (&lt;a href=&#34;https://support.mozilla.org/en-US/questions/990147&#34; target=&#34;_blank&#34; rel=&#34;noopener noreferrer&#34; &gt;or ever?&lt;/a&gt;) for mobile.&lt;/p&gt;
&lt;p&gt;Despite the slower development, I find Thunderbird to be a very powerful client. It has great support for email, calendar, and contacts straight out of the box. Things seem clearly organised, and account-management is super easy. There are no dedicated setups for Gmail, Outlook, etc., but it was able to automatically detect the relevant IMAP/SMTP servers for all of my accounts.&lt;/p&gt;
&lt;p&gt;It’s very unopinionated about ordering, views, threading, and much more - which allows you to set things up the way that works best for you. The interface doesn’t try to be flashy or too clean and I find I am very productive when using it.&lt;/p&gt;
&lt;p&gt;The calendar is easy to use and works with open standards like CalDAV.&lt;/p&gt;
&lt;p&gt;It also has built in support for chat through systems like IRC and XMPP (if you use these types of things), and there’s also a rich ecosystem of plugins to add extra functionality too. It’s certainly the most flexible and powerful of the desktop mail apps I’ve used.&lt;/p&gt;
&lt;p&gt;A few areas where it frustrates are around its performance. When adding a new account it proceeds to automatically download all of the mail headers for that account to be stored locally in its databases. This allows it to support searching and other local tasks, but the process causes the app to run slowly whilst it’s in progress. If you change computer often, or have several machines to setup, then this could be a pain.&lt;/p&gt;
&lt;p&gt;When opening large mail folders containing perhaps several hundreds of thousands of messages - for example my combined “Archive” folder - things get &lt;em&gt;very&lt;/em&gt; slow to the point where it is unusable. However, I don’t really ever need to use these views so this isn’t too much of a problem for me, but for some people this could be a blocker.&lt;/p&gt;
&lt;p&gt;When compared to Apple Mail and Spark, the search function seems very slow. The results returned are quite accurate though, and the fact that results get shown in their own “tab” means that your flow isn’t interruped elsewhere in the app. This is a nice feature.&lt;/p&gt;
&lt;p&gt;Generally, I love Thunderbird. Mozilla is renowned for being privacy-centric and the fact that everything is stored locally gives me more confidence about its security. Of course, it has drawbacks which will put some people off, but it’s good to be supporting open-source software where possible.&lt;/p&gt;
&lt;h2 id=&#34;conclusion&#34;&gt;Conclusion&lt;/h2&gt;
&lt;p&gt;Like a web browser, I think people should be free to continue to try alternative mail clients as their needs and the software features change. The software mentioned above comprise just a small sample, and are focused largely around the Apple ecosystems since these are the devices I happen to be using at the moment.&lt;/p&gt;
&lt;p&gt;Some others I’d like to try are &lt;a href=&#34;https://airmailapp.com&#34; target=&#34;_blank&#34; rel=&#34;noopener noreferrer&#34; &gt;Airmail&lt;/a&gt; and &lt;a href=&#34;https://polymail.io&#34; target=&#34;_blank&#34; rel=&#34;noopener noreferrer&#34; &gt;Polymail&lt;/a&gt;. However, it’d be great to get some feedback on what other people are using. If you have any suggestions then please get in touch using Matrix (@wilw:matrix.wilw.dev) or on Mastodon (&lt;a href=&#34;https://fosstodon.org/@wilw&#34; target=&#34;_blank&#34; rel=&#34;noopener noreferrer&#34; &gt;fosstodon.org/@wilw&lt;/a&gt;).&lt;/p&gt;
</content:encoded>
    </item>
    
    <item>
      <title>The simplicity and flexibility of HTTP for APIs</title>
      <link>https://wilw.dev/blog/2021/03/31/http-simplicity/</link>
      <pubDate>Wed, 31 Mar 2021 22:00:00 +0000</pubDate>
      
      <guid>https://wilw.dev/blog/2021/03/31/http-simplicity/</guid>
      
        <category>100daystooffload</category>
      
        <category>technology</category>
      
        <category>opinion</category>
      
      
      <content:encoded>&lt;h1 id=&#34;simple-and-restful-http-apis&#34;&gt;Simple and RESTful HTTP APIs&lt;/h1&gt;
&lt;p&gt;The &lt;a href=&#34;https://en.wikipedia.org/wiki/Hypertext_Transfer_Protocol&#34; target=&#34;_blank&#34; rel=&#34;noopener noreferrer&#34; &gt;HTTP standard&lt;/a&gt; is an expressive system for network-based computer-computer interaction. It’s a relatively old standard - it started life as HTTP/1.0 in 1996 and the HTTP/1.1 standard was formally specified in 1999. HTTP/2 (2015) introduced efficiencies around &lt;em&gt;how&lt;/em&gt; the data is transmitted between computers, and the still in-draft HTTP/3 builds further on these concepts.&lt;/p&gt;
&lt;p&gt;I won’t go into the nuts and bolts of it, but - essentially - for most applications and APIs, the developer-facing concepts haven’t really changed since HTTP/1.1. By this version, we had all the useful methods required to build powerful and flexible APIs.&lt;/p&gt;
&lt;p&gt;When writing a web service (e.g. a website or a web-based REST API), actions are based around &lt;em&gt;resources&lt;/em&gt;. These are the “things” or “concepts” we are concerned with. For example, if one was to write a to-do list app, two of the concepts might be “to-do list” and “to-do list item”. Generally, such an app might also maintain user accounts and so may have “user” and “session” resources, too, along with others if required.&lt;/p&gt;
&lt;p&gt;In such a service, resources are usually indicated by a &lt;em&gt;path&lt;/em&gt;. This is the bit that comes after the host name (e.g. &lt;code&gt;example.com&lt;/code&gt;) in the URL and are usually mentioned in &lt;em&gt;plural&lt;/em&gt;.&lt;/p&gt;
&lt;p&gt;For example, in our to-do list example, a resource which indicates &lt;em&gt;all&lt;/em&gt; available lists might be given simply by &lt;code&gt;/lists&lt;/code&gt;, and a specific list with ID &lt;code&gt;aaa-bbb-ccc&lt;/code&gt; would be available at &lt;code&gt;/lists/aaa-bbb-ccc&lt;/code&gt;.&lt;/p&gt;
&lt;p&gt;This system allows the engineer to indicate &lt;em&gt;hierarchy&lt;/em&gt; or ownership in the data model. For example, to address all of the list items in a specific list one might use &lt;code&gt;/lists/aaa-bbb-ccc/items&lt;/code&gt;. Then to access an item with ID &lt;code&gt;xxx-yyy-zzz&lt;/code&gt; inside this list you’d use &lt;code&gt;/lists/aaa-bbb-ccc/items/xxx-yyy-zzz&lt;/code&gt;. In many cases, for a simple web service of this type, this would be sufficient - it may not be appropriate to enable addressing a to-do list item directly without the context of its “parent” list.&lt;/p&gt;
&lt;p&gt;Paths may sometimes include other metadata, such as the API version being called, but this can be simply included and described in the documentation.&lt;/p&gt;
&lt;p&gt;In HTTP, &lt;em&gt;methods&lt;/em&gt; describe &lt;em&gt;what&lt;/em&gt; the API consumer wants to do with a resource. Some of the most widely used methods are &lt;code&gt;GET&lt;/code&gt;, &lt;code&gt;POST&lt;/code&gt;, &lt;code&gt;PUT&lt;/code&gt;, &lt;code&gt;DELETE&lt;/code&gt;, and &lt;code&gt;OPTIONS&lt;/code&gt;. These methods are defined in the spec and some clients may handle requests differently based on the method being used. Unlike resources, you can’t define your own methods to use. However, the flexibility provided as-is allows for most services to be built without requiring this level of customisation.&lt;/p&gt;
&lt;p&gt;Some of these have pretty obvious semantic meaning. &lt;code&gt;POST&lt;/code&gt; is typically used to create a new resource (e.g. “post a new to-do list item”) and &lt;code&gt;PUT&lt;/code&gt; is used to update an existing resource.&lt;/p&gt;
&lt;p&gt;This means that, given the combination of our resource addressing and methods, we can express a powerful web service. Structuring your to-do list app using the system described here caters well to typical to-do list actions: creating lists and items (e.g. &lt;code&gt;POST /lists/aaa-bbb-ccc/items&lt;/code&gt;), crossing-off items (probably &lt;code&gt;PUT /lists/aaa-bbb-ccc/xxx-yyy-zzz&lt;/code&gt;), and retrieving, updating, and deleting things in a similar way using the appropriate methods.&lt;/p&gt;
&lt;p&gt;HTTP request &lt;em&gt;headers&lt;/em&gt; can be used to provide authentication information, describe how the client wants information to be returned in the response, along with other ways to further annotate the request being made and to customise the expected response. Of course, the effectiveness of supplying these request headers depends on the server’s own capability and configuration. However, the use of headers should certainly be considered by the engineer whilst planning and building out the service.&lt;/p&gt;
&lt;p&gt;Using standards like these - resources, methods, and headers - in your APIs enables your users (&lt;em&gt;consumers&lt;/em&gt;) to more easily learn and understand how to use your service. This saves them time, helps your service to grow, and means you’ll spend less time dealing with support requests (unless your documentation is really good).&lt;/p&gt;
&lt;h1 id=&#34;custom-implementations&#34;&gt;Custom implementations&lt;/h1&gt;
&lt;p&gt;I think the above system is the most ideal, expressive, learnable and &lt;em&gt;expected&lt;/em&gt; way of building web services.&lt;/p&gt;
&lt;p&gt;However, HTTP is flexible, and your server-side code can - in theory - do whatever you want it to, no matter what the request path, method, and headers are. But I don’t really understand why one would want to.&lt;/p&gt;
&lt;p&gt;I recently &lt;a href=&#34;https://wilw.dev/blog/2021/02/24/google-photos-pcloud&#34;&gt;migrated my photos collection to pCloud&lt;/a&gt;, and wanted to explore their API to see if I could also use the service for programmatically backing-up other things, too.&lt;/p&gt;
&lt;p&gt;Unfortunately I am unable to actually use their API, since I use two-factor authentication on pCloud and the API doesn’t seem to work if this extra layer of security is in-place. However, whilst researching I discovered that pCloud’s API is an example of a service that seems to defy the standards one is usually familiar with.&lt;/p&gt;
&lt;p&gt;For example, it appears that it’s perfectly acceptable to use &lt;code&gt;POST https://api.pcloud.com/deletefile?fileid=myfile&lt;/code&gt; to delete a file or &lt;code&gt;GET https://api.pcloud.com/renamefolder?path=oldfolder&amp;topath=newfolder&lt;/code&gt; to rename a folder.&lt;/p&gt;
&lt;p&gt;There’s nothing &lt;em&gt;technically&lt;/em&gt; wrong with this implementation, especially given the fact that I’m sure it works. It perhaps makes it easier to route requests through to the correct internal functions. However it just feels &lt;em&gt;inelegant&lt;/em&gt; to me, and it seems to focus more on what’s easier for them rather than their users.&lt;/p&gt;
&lt;p&gt;The &lt;a href=&#34;https://docs.pcloud.com/methods/file&#34; target=&#34;_blank&#34; rel=&#34;noopener noreferrer&#34; &gt;page that lists file operations&lt;/a&gt; could instead show a couple of simple example paths and then rely on request &lt;em&gt;methods&lt;/em&gt; and parameters to describe available options.&lt;/p&gt;
&lt;p&gt;I don’t mean to pick on pCloud - the service itself is great and I’m sure the API works nicely. I plan to continue using the service via its web UI and official clients. I only bring it up because it seems odd to re-invent the wheel.&lt;/p&gt;
&lt;p&gt;I’m completely on-board with the notion of discouraging system and process monopoly, but I don’t think this is the same thing. The web is formed from a set of open standards that anyone can comment on or help contribute to.&lt;/p&gt;
&lt;h1 id=&#34;good-implementation-examples&#34;&gt;“Good” implementation examples&lt;/h1&gt;
&lt;p&gt;The web is full of services that expose sensible and learnable APIs.&lt;/p&gt;
&lt;p&gt;An example I always love is the Stripe API - arguably a much more complex service than pCloud. However its &lt;a href=&#34;https://stripe.com/docs/api/charges&#34; target=&#34;_blank&#34; rel=&#34;noopener noreferrer&#34; &gt;simple “compliant” API&lt;/a&gt; makes credit card payments - and loads more - very easy to integrate with.&lt;/p&gt;
&lt;p&gt;The &lt;a href=&#34;https://developer.spotify.com/documentation/web-api/reference&#34; target=&#34;_blank&#34; rel=&#34;noopener noreferrer&#34; &gt;Spotify web API&lt;/a&gt; also looks useful, though I haven’t used that before myself.&lt;/p&gt;
&lt;h1 id=&#34;beyond-rest&#34;&gt;Beyond REST&lt;/h1&gt;
&lt;p&gt;REST has been a cornerstone of the web over the past couple of decades, and I think there is still very much a space for it - both for now and in the near future. Its flexibility has allowed it to remain useful across industries and settings - from small private IoT setups through to highly-secure enterprise-enterprise systems.&lt;/p&gt;
&lt;p&gt;There are movements to begin using other technologies that may be better suited to the future of the web and communication - particularly as things continue to scale. Efforts such as &lt;a href=&#34;https://graphql.org&#34; target=&#34;_blank&#34; rel=&#34;noopener noreferrer&#34; &gt;GraphQL&lt;/a&gt;, &lt;a href=&#34;https://netflix.github.io/falcor&#34; target=&#34;_blank&#34; rel=&#34;noopener noreferrer&#34; &gt;Netflix’s Falcor project&lt;/a&gt;, and even &lt;a href=&#34;https://en.wikipedia.org/wiki/Remote_procedure_call&#34; target=&#34;_blank&#34; rel=&#34;noopener noreferrer&#34; &gt;RPC&lt;/a&gt; provide alternatives for when REST isn’t the most appropriate solution.&lt;/p&gt;
&lt;p&gt;However, if building a web API that you want other people to use, and which would be well suited to REST, then I always think it’s worth sticking to these HTTP standards as much as possible.&lt;/p&gt;
</content:encoded>
    </item>
    
    <item>
      <title>PinePhone and PineTime</title>
      <link>https://wilw.dev/blog/2021/03/27/pinephone-pinetime/</link>
      <pubDate>Sat, 27 Mar 2021 16:31:00 +0000</pubDate>
      
      <guid>https://wilw.dev/blog/2021/03/27/pinephone-pinetime/</guid>
      
        <category>100daystooffload</category>
      
        <category>technology</category>
      
        <category>pinephone</category>
      
        <category>life</category>
      
      
      <content:encoded>&lt;h2 id=&#34;pre-ordering-the-pinephone-beta&#34;&gt;Pre-ordering the PinePhone Beta&lt;/h2&gt;
&lt;p&gt;Earlier this week I ordered a &lt;a href=&#34;https://www.pine64.org/pinephone&#34; target=&#34;_blank&#34; rel=&#34;noopener noreferrer&#34; &gt;PinePhone&lt;/a&gt;, which recently became &lt;a href=&#34;https://pine64.com/product-category/smartphones&#34; target=&#34;_blank&#34; rel=&#34;noopener noreferrer&#34; &gt;available as a Beta Edition&lt;/a&gt;.&lt;/p&gt;
&lt;p&gt;I’ve been excitedly following the progress of the PinePhone for some time now. I’ve joined various Matrix rooms, subscribed to &lt;a href=&#34;https://linmob.net&#34; target=&#34;_blank&#34; rel=&#34;noopener noreferrer&#34; &gt;blogs&lt;/a&gt;, and started listening to the &lt;a href=&#34;https://www.pine64.org/pinetalk&#34; target=&#34;_blank&#34; rel=&#34;noopener noreferrer&#34; &gt;PineTalk podcast&lt;/a&gt;. The phone is a hackable device that runs plain old Linux - not an Android variant - and thus helps users escape from the grasp of the Google and Apple ecosystems.&lt;/p&gt;
&lt;p&gt;Other similar devices exist - such as the &lt;a href=&#34;https://puri.sm/products/librem-5&#34; target=&#34;_blank&#34; rel=&#34;noopener noreferrer&#34; &gt;Librem 5 from Purism&lt;/a&gt; - however the unopinionated nature of the PinePhone, and its cost ($150 compared to the Librem’s $800), make the Pine64 offering much more attractive to me.&lt;/p&gt;
&lt;p&gt;I understand that the phone and software are still under very active development, and I fully expect that the phone is not yet ready to become a daily driver. However I am excited to try it out, support the project, and contribute where I can. The potential of this movement is huge.&lt;/p&gt;
&lt;h2 id=&#34;some-thoughts-on-pinetime&#34;&gt;Some thoughts on PineTime&lt;/h2&gt;
&lt;p&gt;Whilst researching the PinePhone, I stumbled across the &lt;a href=&#34;https://www.pine64.org/pinetime&#34; target=&#34;_blank&#34; rel=&#34;noopener noreferrer&#34; &gt;PineTime smartwatch&lt;/a&gt;. This is a wearable device also from Pine64, which aims to offer an open-source and hackable system in a similar vein to the PinePhone.&lt;/p&gt;
&lt;p&gt;Pine64 offers the device for purchase but fully acknowledges that it is not yet ready for daily use, and encourages interested people to instead purchase the &lt;a href=&#34;https://pine64.com/product/pinetime-dev-kit&#34; target=&#34;_blank&#34; rel=&#34;noopener noreferrer&#34; &gt;Development Kit&lt;/a&gt; so that they can learn more or contribute to the project.&lt;/p&gt;
&lt;p&gt;The device aims to offer health tracking solutions (since it includes a step counter and heart rate detector) and notifications, and so the intention is for it to offer a similar experience to other smart watches - except with much more freedom.&lt;/p&gt;
&lt;p&gt;The open and community-driven nature of the device could take it any number of ways.&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;We envision the PineTime as a companion for not only your PinePhone but also for your favorite devices — any phone, tablet, or even PC - pine64.org/pinetime&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;This vision seems to embody the Pine64 philosophy that we see across all of their products. I’m not the right person to be able to contrubute much to the project in its current stage (I don’t have much experience with developing on embedded operating systems), but I look forward to seeing how it progresses and hopefully getting more involved slightly further down the line.&lt;/p&gt;
</content:encoded>
    </item>
    
    <item>
      <title>The Great Alone by Kristin Hannah</title>
      <link>https://wilw.dev/blog/2021/03/23/the-great-alone/</link>
      <pubDate>Tue, 23 Mar 2021 19:45:00 +0000</pubDate>
      
      <guid>https://wilw.dev/blog/2021/03/23/the-great-alone/</guid>
      
        <category>100daystooffload</category>
      
        <category>book</category>
      
      
      <content:encoded>&lt;p&gt;&lt;a href=&#34;https://www.goodreads.com/book/show/34912895-the-great-alone&#34; target=&#34;_blank&#34; rel=&#34;noopener noreferrer&#34; &gt;&lt;em&gt;The Great Alone&lt;/em&gt;&lt;/a&gt; by &lt;a href=&#34;https://www.goodreads.com/author/show/54493.Kristin_Hannah&#34; target=&#34;_blank&#34; rel=&#34;noopener noreferrer&#34; &gt;Kristin Hannah&lt;/a&gt; is a book set out in the Alaskan wild. It tells the story of a young family that move in order to live off-the-grid after the father returns from being a prisoner of war in the Vietnam war.&lt;/p&gt;
&lt;p&gt;&lt;img src=&#34;https://wilw.dev/media/blog/the_great_alone.jpg&#34; alt=&#34;The Great Alone book cover&#34;&gt;&lt;/p&gt;
&lt;p&gt;The book mostly focuses on the viewpoint of the daughter, Leni, who is thirteen years old when she moves with her mother and father. The story tells how Leni adapts and grows into her new Alaskan life over the years, whilst at the same time trying to navigate some of the perils at home in her family cabin. Leni and her family meet and grow close to different members of the local community, in which there are a variety of views regarding the types of people that should be allowed to come to Alaska.&lt;/p&gt;
&lt;p&gt;The book certainly has its dark moments, and there is an ongoing sense of violence and intensity. At the same time, the author wonderfully describes the peacefulness of the environment, and the wildness of the Alaskan landscape, the wildlife, the weather, the sky, and the sea. It is clearly a place where humans and nature meet, and a place where - if people are to live off the land - they must learn and respect it and all it has to offer.&lt;/p&gt;
&lt;p&gt;After all, in Alaska you can only ever make one mistake. The second one will kill you.&lt;/p&gt;
&lt;p&gt;I loved the book and its intertwining themes of love, family drama (and more), forgiveness, wilderness, comradeship, and escapism. The author makes you feel frustrated with some of the decisions made by the characters in one moment, and the next you are cheering them on from behind the pages.&lt;/p&gt;
&lt;p&gt;With everything that goes on in the story - the town and its community of interesting characters - it isn’t always obvious where the title of the book comes from. However, as you progress further you realise that it’s not just the landscape and geography that can evoke loneliness; the feeling can be more the result of the actions of others and having to keep secrets about what goes on behind closed doors.&lt;/p&gt;
</content:encoded>
    </item>
    
    <item>
      <title>Running your own Matrix homeserver</title>
      <link>https://wilw.dev/blog/2021/03/22/host-matrix/</link>
      <pubDate>Mon, 22 Mar 2021 11:50:00 +0000</pubDate>
      
      <guid>https://wilw.dev/blog/2021/03/22/host-matrix/</guid>
      
        <category>100daystooffload</category>
      
        <category>technology</category>
      
        <category>selfhost</category>
      
      
        <enclosure url="https://wilw.dev/media/blog/header-host-matrix.png" type="image/png"/>
      
      <content:encoded>&lt;h1 id=&#34;why-use-decentralised-communication-services&#34;&gt;Why use decentralised communication services&lt;/h1&gt;
&lt;p&gt;Centralised communication services, such as Telegram, Signal, and Whatsapp, offer convenient means to chat to friends and family using your personal devices. However these services also come with a number of pitfalls that are worth considering. For example;&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;Many of these services are linked to your phone number, which can affect your privacy.&lt;/li&gt;
&lt;li&gt;They can be invasive with your contacts (&lt;em&gt;“Jane Doe is now using Telegram!”&lt;/em&gt;).&lt;/li&gt;
&lt;li&gt;They usually require you to use proprietary client software. If your OS/platform isn’t supported then you can’t use that service.&lt;/li&gt;
&lt;li&gt;They typically require that everyone using the service has to use the same client software.&lt;/li&gt;
&lt;li&gt;They can be unreliable (Whatsapp frequently has downtime).&lt;/li&gt;
&lt;li&gt;They are invasive and collect data about you (particularly Whatsapp). If you don’t pay for the service, then &lt;em&gt;you&lt;/em&gt; are the product.&lt;/li&gt;
&lt;li&gt;Even though Signal is encrypted end-to-end, its servers are based in the US and are subject to the laws there. Also their open-source server-side software appears to &lt;a href=&#34;https://github.com/signalapp/Signal-Server&#34; target=&#34;_blank&#34; rel=&#34;noopener noreferrer&#34; &gt;not have been updated&lt;/a&gt; for some time.&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;There are, of course, other factors on both sides that you may want to consider. It can be hard to move away from these services - after all, there’s no point using a system that no-one else you need to talk to uses.&lt;/p&gt;
&lt;p&gt;However, for some people, being able to avoid these issues can be important. One way to do so is to participate in a (preferably open-source) decentralised communication service in which the entire network is not owned by a single entity and where data colleciton is not the business model. This also helps prevent unstability and downtime, since there is not a single point of failure.&lt;/p&gt;
&lt;p&gt;This is analogous to using services such as Mastodon and Pixelfed over Twitter and Instagram, respectively - the underlying software is open-source and anyone can host an “instance”. In these cases, each instance can communicate with others using the &lt;a href=&#34;https://en.wikipedia.org/wiki/ActivityPub&#34; target=&#34;_blank&#34; rel=&#34;noopener noreferrer&#34; &gt;ActivityPub&lt;/a&gt; protocol. In this post I will talk about another protocol that offers decentralised and federated encrypted communication.&lt;/p&gt;
&lt;h1 id=&#34;the-matrix-protocol&#34;&gt;The Matrix protocol&lt;/h1&gt;
&lt;p&gt;The &lt;a href=&#34;https://www.matrix.org&#34; target=&#34;_blank&#34; rel=&#34;noopener noreferrer&#34; &gt;Matrix protocol&lt;/a&gt; is one example of a standard for real-time decentralised communication. Since the standard is open, anyone can build server and client software that enables end-to-end encrypted communication between two or more people. Another example of a similar protocol is &lt;a href=&#34;https://en.wikipedia.org/wiki/XMPP&#34; target=&#34;_blank&#34; rel=&#34;noopener noreferrer&#34; &gt;XMPP&lt;/a&gt;, which is also very popular and has been around (in its earlier forms) since 1999.&lt;/p&gt;
&lt;p&gt;When using Matrix, you belong to a “homeserver”. This is where your messages and some account details are stored. However, since Matrix is a &lt;em&gt;federated&lt;/em&gt; protocol, you can use your account to communicate with others on your homeserver as well as people from other homeservers that federate with yours.&lt;/p&gt;
&lt;p&gt;The standard was introduced back in 2014, and by now there is an established ecosystem of software available for use. In fact, you can use &lt;a href=&#34;https://element.io/get-started&#34; target=&#34;_blank&#34; rel=&#34;noopener noreferrer&#34; &gt;Element&lt;/a&gt; on your device and get started by joining an existing homeserver right now.&lt;/p&gt;
&lt;p&gt;Additionally, if you don’t want the hassle of self-hosting yet another service, then &lt;a href=&#34;https://element.io/matrix-services&#34; target=&#34;_blank&#34; rel=&#34;noopener noreferrer&#34; &gt;Element also provides plans&lt;/a&gt; that allow you to run your own homeserver on managed hosting.&lt;/p&gt;
&lt;h1 id=&#34;self-hosting-a-matrix-homeserver&#34;&gt;Self-hosting a Matrix homeserver&lt;/h1&gt;
&lt;p&gt;If you want more control over your data, you may opt to self-host your own homeserver that implements the Matrix standard. Even if you self-host you can still take advantage of the protocol’s federation features and communicate with people on other homeservers.&lt;/p&gt;
&lt;p&gt;The resource requirement for Matrix servers is a bit on the heavier side (especially when compared to the lighter XMPP servers). However if you already run a small-ish VPS anyway (as I do for things like Nextcloud), and if you only expect one or two people to be enrolled directly on your homeserver, then you can certainly host Matrix on that same VPS without too much trouble. For reference, I have a single $10 server from &lt;a href=&#34;https://www.linode.com&#34; target=&#34;_blank&#34; rel=&#34;noopener noreferrer&#34; &gt;Linode&lt;/a&gt;, which happily runs Matrix alongside a number of other services.&lt;/p&gt;
&lt;p&gt;The &lt;a href=&#34;https://github.com/matrix-org/synapse&#34; target=&#34;_blank&#34; rel=&#34;noopener noreferrer&#34; &gt;Synapse project&lt;/a&gt; is probably one of the most robust and feature-complete homeserver implementations, and is the one I’ll talk about in this post. They also offer an officially supported &lt;a href=&#34;https://hub.docker.com/r/matrixdotorg/synapse&#34; target=&#34;_blank&#34; rel=&#34;noopener noreferrer&#34; &gt;Docker image&lt;/a&gt;, which is what I would recommend using to keep things in one place.&lt;/p&gt;
&lt;h2 id=&#34;homeserver-name&#34;&gt;Homeserver name&lt;/h2&gt;
&lt;p&gt;Firstly, I’d recommend setting up a domain (either an existing one or a new one) and then updating your DNS such that the relevant entry points to your server.&lt;/p&gt;
&lt;p&gt;It is important to think about the domain name you choose for your homeserver, since this cannot be changed later. &lt;a href=&#34;https://github.com/matrix-org/synapse/blob/master/INSTALL.md#choosing-your-server-name&#34; target=&#34;_blank&#34; rel=&#34;noopener noreferrer&#34; &gt;Matrix recommends&lt;/a&gt; using your root domain name itself rather than a subdomain for your homeserver name. However if you already host a website using your full domain name you will need some extra configuration to make it work properly. I personally don’t, as I wanted an easier setup!&lt;/p&gt;
&lt;h2 id=&#34;exposing-ports-and-preparing-tls-certificates&#34;&gt;Exposing ports and preparing TLS certificates&lt;/h2&gt;
&lt;p&gt;In order to configure HTTPS, I’d recommend setting up an Nginx container or server as a reverse proxy and issuing certificates using Let’s Encrypt. The Matrix protocol uses standard port 443 for communication with clients (e.g. from an app) - known as the “client port” - and port 8448 for communication with other homeservers (the “federation port”).&lt;/p&gt;
&lt;p&gt;You may wish to read some of the &lt;a href=&#34;https://github.com/matrix-org/synapse/blob/master/docs/reverse_proxy.md&#34; target=&#34;_blank&#34; rel=&#34;noopener noreferrer&#34; &gt;official documentation&lt;/a&gt; on setting up a reverse-proxy, but I’ll run through roughly what I do below.&lt;/p&gt;
&lt;p&gt;Depending on your Nginx setup, you may need a couple of &lt;code&gt;server&lt;/code&gt; blocks similar to the following to configure your reverse proxy (assuming your homeserver name is “example.com”):&lt;/p&gt;
&lt;pre tabindex=&#34;0&#34;&gt;&lt;code&gt;server {
  listen      80;
  listen      [::]:80;
  server_name example.com;
  return 301  https://$host$request_uri;
} 

server {
  listen      443 ssl;
  listen      [::]:443 ssl;
  listen      8448 ssl;
  listen      [::]:8448 ssl;

  server_name example.com;

  ssl_certificate     /path/to/fullchain.pem;
  ssl_certificate_key /path/to/privkey.pem; 

  location / {
    proxy_pass http://synapse:8008;
    proxy_set_header X-Forwarded-For $remote_addr;
    proxy_set_header X-Forwarded-Proto $scheme;
    proxy_set_header Host $host;
    client_max_body_size 50M;
  }
}
&lt;/code&gt;&lt;/pre&gt;&lt;p&gt;If you run Nginx as a Docker container remember also to expose port 8448 alongside 443.&lt;/p&gt;
&lt;p&gt;Synapse uses port 8008 for HTTP communication, to which we forward requests received on both the secure client and federation ports. In the example above, &lt;code&gt;synapse&lt;/code&gt; is the name of the container that runs my homeserver, as we’ll cover next. Again, depending on your setup, and whether you choose to use Docker, you may need to change this value so that your reverse proxy can route through to port 8008 on your homeserver.&lt;/p&gt;
&lt;h2 id=&#34;generate-a-configuration-file&#34;&gt;Generate a configuration file&lt;/h2&gt;
&lt;p&gt;The next step is to generate your homesever config file. I recommend firstly creating a directory to hold your synapse data (e.g. &lt;code&gt;mkdir synapse_data&lt;/code&gt;). We’ll mount this to &lt;code&gt;/data&lt;/code&gt; on the target container in order for the configuration file to be created.&lt;/p&gt;
&lt;p&gt;The configuration file can be generated using Docker:&lt;/p&gt;
&lt;pre tabindex=&#34;0&#34;&gt;&lt;code&gt;docker run -it --rm \
  -v synapse_data:/data
  -e SYNAPSE_SERVER_NAME=example.com \
  -e SYNAPSE_REPORT_STATS=yes \
  matrixdotorg/synapse:latest generate
&lt;/code&gt;&lt;/pre&gt;&lt;p&gt;Once this completes, your &lt;code&gt;synapse_data&lt;/code&gt; directory should contain a &lt;code&gt;homeserver.yaml&lt;/code&gt; file. Feel free to read through this and check out the &lt;a href=&#34;https://github.com/matrix-org/synapse&#34; target=&#34;_blank&#34; rel=&#34;noopener noreferrer&#34; &gt;documentation&lt;/a&gt; for ways in which it can be modified.&lt;/p&gt;
&lt;h2 id=&#34;run-the-homeserver&#34;&gt;Run the homeserver&lt;/h2&gt;
&lt;p&gt;Finally, we can now run the homeserver. Depending on your reverse proxy setup (and whether you are containerising anything else), you may need to configure your Docker networks, but generally you can just execute the following to get your homeserver running:&lt;/p&gt;
&lt;pre tabindex=&#34;0&#34;&gt;&lt;code&gt;docker run -d \
  -v synapse_data:/data
  --name synapse \
  --restart always \
  matrixdotorg/synapse:latest
&lt;/code&gt;&lt;/pre&gt;&lt;p&gt;If everything went well (and assuming your reverse proxy is also now up and running), you should be able to use your web browser to visit your Matrix domain (we used “example.com” above) and see a page that looks like this:&lt;/p&gt;
&lt;p&gt;&lt;img src=&#34;https://wilw.dev/media/blog/matrix.png&#34; alt=&#34;Matrix homeserver confirmation page&#34;&gt;&lt;/p&gt;
&lt;h2 id=&#34;creating-your-user-account&#34;&gt;Creating your user account&lt;/h2&gt;
&lt;p&gt;As long as your homeserver is configured to accept user registrations (via the &lt;code&gt;enable_registration&lt;/code&gt; directive in &lt;code&gt;homeserver.yaml&lt;/code&gt;), you should be able to &lt;a href=&#34;https://matrix.org/clients&#34; target=&#34;_blank&#34; rel=&#34;noopener noreferrer&#34; &gt;download a client&lt;/a&gt; (or use the &lt;a href=&#34;https://app.element.io&#34; target=&#34;_blank&#34; rel=&#34;noopener noreferrer&#34; &gt;Element webapp&lt;/a&gt;) and register your first user account.&lt;/p&gt;
&lt;p&gt;Once logged-in you can join rooms, invite people, and begin communicating with others.&lt;/p&gt;
&lt;h1 id=&#34;conclusion&#34;&gt;Conclusion&lt;/h1&gt;
&lt;p&gt;This post aims to be a rough introduction to running your own Matrix homeserver. The Synapse software offers a variety of ways to tailor your instance, and so it is certainly worth becoming familiar with some of &lt;a href=&#34;https://github.com/matrix-org/synapse&#34; target=&#34;_blank&#34; rel=&#34;noopener noreferrer&#34; &gt;the documentation&lt;/a&gt; to ensure you have configured things the way you need.&lt;/p&gt;
&lt;p&gt;If you want to get in touch then you can send me a message using Matrix (@wilw:matrix.wilw.dev) or on Mastodon (&lt;a href=&#34;https://fosstodon.org/@wilw&#34; target=&#34;_blank&#34; rel=&#34;noopener noreferrer&#34; &gt;@wilw@fosstodon.org&lt;/a&gt;).&lt;/p&gt;
</content:encoded>
    </item>
    
    <item>
      <title>Blood, Sweat, and Pixels by Jason Schreier</title>
      <link>https://wilw.dev/blog/2021/03/17/blood-sweat-pixels/</link>
      <pubDate>Wed, 17 Mar 2021 19:23:00 +0000</pubDate>
      
      <guid>https://wilw.dev/blog/2021/03/17/blood-sweat-pixels/</guid>
      
        <category>100daystooffload</category>
      
        <category>book</category>
      
      
      <content:encoded>&lt;p&gt;This post contains some of my thoughts on the book &lt;em&gt;&lt;a href=&#34;https://www.goodreads.com/book/show/34376766-blood-sweat-and-pixels&#34; target=&#34;_blank&#34; rel=&#34;noopener noreferrer&#34; &gt;Blood, Sweat, and Pixels&lt;/a&gt;&lt;/em&gt; by &lt;a href=&#34;https://www.goodreads.com/author/show/16222011.Jason_Schreier&#34; target=&#34;_blank&#34; rel=&#34;noopener noreferrer&#34; &gt;Jason Schreier&lt;/a&gt;.&lt;/p&gt;
&lt;p&gt;&lt;img src=&#34;https://wilw.dev/media/blog/blood-sweat-pixels.jpg&#34; alt=&#34;Blood, Sweat, and Pixels book cover&#34;&gt;&lt;/p&gt;
&lt;p&gt;This book contains a number of stories about how some of the most well-known (and other less well-known) video games are made. The book’s subtitle, “&lt;em&gt;The Triumphant, Turbulent Stories Behind How Video Games Are Made&lt;/em&gt;”, sums it up pretty well.&lt;/p&gt;
&lt;p&gt;Working in the software industry myself, I often hear about the notion of “crunch time”, which is a term we’ve borrowed from the game devleopment industry at times when critical updates, fixes, or deadlines are pressing. However, after reflecting on the stories in this book, it makes me realise that the “crunches” we suffer are nothing to the crunch and stresses experienced by game developers in many small teams and large development studios alike.&lt;/p&gt;
&lt;p&gt;Every chapter explains in detail the pain and reward faced by game developers and management teams on an ongoing basis. The developer skill and expertise required by game studios, and the time and size of the required resource, helps to explain the huge financial impact these projects have.&lt;/p&gt;
&lt;p&gt;It’s no wonder why such harsh deadlines are set. In many cases it’s a matter of “life or death”: either the game gets released on time or there is no game at all and everyone has to lose their job - even in large well-funded companies.&lt;/p&gt;
&lt;p&gt;I loved the stories of the groups of developers that ended up leaving their well-paid (but stressful) jobs in order to start something by themselves as a smaller group - not quite realising at the start what they were letting themselves in for.&lt;/p&gt;
&lt;p&gt;I enjoyed the story behind the development of the game &lt;em&gt;Stardew Valley&lt;/em&gt;. This is a game I love and have played for hours on my Switch - not knowing really (or fully appreciating) where the game came from and all the time spent by its solo developer and the stress that went on behind the scenes.&lt;/p&gt;
&lt;p&gt;The background to the development of &lt;em&gt;The Witcher 3&lt;/em&gt; was also fascinating; how the relatively small but super-ambitious studio &lt;a href=&#34;https://en.cdprojektred.com&#34; target=&#34;_blank&#34; rel=&#34;noopener noreferrer&#34; &gt;CD Projekt Red&lt;/a&gt; successfully brought to the world stage the Polish much-loved fantasy world.&lt;/p&gt;
&lt;p&gt;The book was great, and well-narrated by &lt;a href=&#34;https://en.wikipedia.org/wiki/Ray_Chase_%28voice_actor%29&#34; target=&#34;_blank&#34; rel=&#34;noopener noreferrer&#34; &gt;Ray Chase&lt;/a&gt; (I listened to the &lt;a href=&#34;https://www.audible.co.uk/pd/Blood-Sweat-and-Pixels-Audiobook/B075KG1SBW&#34; target=&#34;_blank&#34; rel=&#34;noopener noreferrer&#34; &gt;Audible version&lt;/a&gt;). I only wish there were more stories (it only took a few days to get through), but I appreciate the effort the author went into with researching and interviewing some of the key people involved. It is an excellent insight into how parts of the game industry work.&lt;/p&gt;
</content:encoded>
    </item>
    
    <item>
      <title>The Tildeverse</title>
      <link>https://wilw.dev/blog/2021/03/15/tildeverse/</link>
      <pubDate>Mon, 15 Mar 2021 11:05:00 +0000</pubDate>
      
      <guid>https://wilw.dev/blog/2021/03/15/tildeverse/</guid>
      
        <category>100daystooffload</category>
      
        <category>technology</category>
      
      
      <content:encoded>&lt;h2 id=&#34;the-last-twenty-years-of-internet-evolution&#34;&gt;The last twenty years of internet evolution&lt;/h2&gt;
&lt;p&gt;Although I was still somewhere between being of single-digit age and a young teen back in the ’90s and early ’00s, I still fondly remember discovering and becoming a small part of the flourishing community of personal, themed, and hobby websites that connected the web.&lt;/p&gt;
&lt;p&gt;We were even given basic server space in school and the wider internet was thriving with &lt;a href=&#34;https://en.wikipedia.org/wiki/Yahoo!_GeoCities&#34; target=&#34;_blank&#34; rel=&#34;noopener noreferrer&#34; &gt;GeoCities&lt;/a&gt; and communities grew around services like &lt;a href=&#34;http://www.neopets.com&#34; target=&#34;_blank&#34; rel=&#34;noopener noreferrer&#34; &gt;Neopets&lt;/a&gt;. Everyday, after school, we’d go home and continue our playground conversations over &lt;a href=&#34;https://en.wikipedia.org/wiki/Windows_Live_Messenger&#34; target=&#34;_blank&#34; rel=&#34;noopener noreferrer&#34; &gt;MSN Messenger&lt;/a&gt; (after waiting for the dial-up modem to complete its connection, of course). The internet felt small and personal (even if you didn’t use your real name or identity) and &lt;em&gt;exciting&lt;/em&gt;.&lt;/p&gt;
&lt;p&gt;For those more tech-aware than I during those days there were also the established &lt;a href=&#34;https://en.wikipedia.org/wiki/Bulletin_board_system&#34; target=&#34;_blank&#34; rel=&#34;noopener noreferrer&#34; &gt;BBS systems&lt;/a&gt;, &lt;a href=&#34;https://en.wikipedia.org/wiki/Internet_Relay_Chat&#34; target=&#34;_blank&#34; rel=&#34;noopener noreferrer&#34; &gt;IRC&lt;/a&gt; (which is still very much in active use), and several other types of available internet and communication services.&lt;/p&gt;
&lt;p&gt;Over the years since then we’ve obviously seen the introduction and growth of tech companies, which have exploded into nearly every corner of the internet. Some have come and gone, but many are here still and continue to grow. We’re now at a point where many of these services are almost a full “internet” (as it was back in the day) by themselves: on Facebook you can host a page for yourself or your business, you can engage with any nunber of other apps through your Facebook account, you can chat in real-time with individuals or groups of friends and family, and much more.&lt;/p&gt;
&lt;p&gt;In the developing world, many people see the &lt;a href=&#34;https://medium.com/swlh/in-the-developing-world-facebook-is-the-internet-14075bfd8c5e&#34; target=&#34;_blank&#34; rel=&#34;noopener noreferrer&#34; &gt;internet and Facebook&lt;/a&gt; as being entirely analogous such that new mobile handsets are sold with the app pre-installed on their device and cellular carriers &lt;a href=&#34;https://www.fool.com/investing/2020/05/22/facebook-expanded-internet-access-africa-1-billion.aspx&#34; target=&#34;_blank&#34; rel=&#34;noopener noreferrer&#34; &gt;sometimes provide free access to the platform&lt;/a&gt; as part of their data plan.&lt;/p&gt;
&lt;p&gt;This boom (invasion?) has completely changed the way the internet works for day-to-day users. Although these companies and their huge marketing teams have facilitated the growth of adoption of technology for community and communication, it has come at a cost. When using these services, the internet no longer feels personal and exciting.&lt;/p&gt;
&lt;p&gt;For many people - particularly those who grew up with this state of the world or those who never fully engaged before Web 2.0 - this is fine and not a problem. They would likely laugh at the simplicity and “slowness” of the “old internet” compared to the flashy, speedy and engaging platforms they are used to interacting with for several hours every day.&lt;/p&gt;
&lt;h2 id=&#34;community-through-the-_tildeverse_&#34;&gt;Community through the &lt;em&gt;Tildeverse&lt;/em&gt;&lt;/h2&gt;
&lt;p&gt;However, there are also many of us who miss the &lt;em&gt;quality&lt;/em&gt; and &lt;em&gt;meaningfulness&lt;/em&gt; of the smaller and slower web. Since joining Mastodon a couple of years back, it’s been great to be part of a movement that actively encourages the growth and maintenance of personal websites, blogs, distributed systems, and the self-hosted services that help promote these ideologies.&lt;/p&gt;
&lt;p&gt;Movements and concepts such as the &lt;a href=&#34;https://ar.al/2020/08/07/what-is-the-small-web&#34; target=&#34;_blank&#34; rel=&#34;noopener noreferrer&#34; &gt;Small Web&lt;/a&gt;, the &lt;a href=&#34;https://indieweb.org&#34; target=&#34;_blank&#34; rel=&#34;noopener noreferrer&#34; &gt;Indie Web&lt;/a&gt;, and even initiaives like &lt;a href=&#34;https://gemini.circumlunar.space&#34; target=&#34;_blank&#34; rel=&#34;noopener noreferrer&#34; &gt;Project Gemini&lt;/a&gt; have all helped to raise awareness around the fact that there is still a large number of people interested in promoting the ideas around the &lt;a href=&#34;https://jackcheng.com/essays/the-slow-web&#34; target=&#34;_blank&#34; rel=&#34;noopener noreferrer&#34; &gt;slow web&lt;/a&gt;, and building a real sense of &lt;em&gt;community&lt;/em&gt;.&lt;/p&gt;
&lt;p&gt;Also part of this movement is the notion of the &lt;em&gt;Tildeverse&lt;/em&gt;. The &lt;a href=&#34;https://tildeverse.org&#34; target=&#34;_blank&#34; rel=&#34;noopener noreferrer&#34; &gt;Tildeverse&lt;/a&gt; draws some inspiration from &lt;a href=&#34;https://www.pubnix.net&#34; target=&#34;_blank&#34; rel=&#34;noopener noreferrer&#34; &gt;PubNix&lt;/a&gt; and stems from building community through “belonging” - similar to how one might feel when interacting with the &lt;a href=&#34;https://en.wikipedia.org/wiki/Fediverse&#34; target=&#34;_blank&#34; rel=&#34;noopener noreferrer&#34; &gt;Fediverse&lt;/a&gt;.&lt;/p&gt;
&lt;p&gt;The Tildeverse is an opportunity for people to &lt;em&gt;donate&lt;/em&gt; server resources by provisioning and managing a *nix system (e.g. Linux, BSD, or similar), on which members of that &lt;em&gt;tilde community&lt;/em&gt; can have a user account that they can access using programs such as &lt;a href=&#34;https://en.wikipedia.org/wiki/SSH_%28Secure_Shell%29&#34; target=&#34;_blank&#34; rel=&#34;noopener noreferrer&#34; &gt;SSH&lt;/a&gt;.&lt;/p&gt;
&lt;p&gt;The name is derived from the fact that the tilde symbol (&lt;code&gt;~&lt;/code&gt;) is used to denote a user’s &lt;em&gt;home directory&lt;/em&gt; on UNIX-like systems that offer multiuser functionality (e.g. &lt;code&gt;~will&lt;/code&gt;). On such servers, users can use their account and home directory to publish a website, a Gemini capsule, use tools to chat with other members via IRC or internal mail, or take advantage of any number of other services the server administrators may offer.&lt;/p&gt;
&lt;p&gt;To join, it is recommended to first identify a community you feel you can contribute positively towards. Many servers don’t require payment to join (although there are often options to make donations to help contribute towards the running costs), but it is usually expected that you help foster the sense of community by actively engaging with others, posting interesting or useful content, or by abiding by other “rules” that may be in place.&lt;/p&gt;
&lt;p&gt;If you have found a community you’d like to join, a typical registration is often achieved by emailing the server administrators with your desired username and an SSH public key. If and when your registration is accepted, you can then use the corresponding private key to login and begin to engage with the community.&lt;/p&gt;
&lt;p&gt;Many such communities, such as &lt;a href=&#34;https://tilde.club&#34; target=&#34;_blank&#34; rel=&#34;noopener noreferrer&#34; &gt;tilde.club&lt;/a&gt;, list some of the users’ home directories as webpages. This lets you get an idea of the community before choosing to join. Many homepages (though this isn’t limited to the Tildeverse) include a &lt;em&gt;webring&lt;/em&gt;, which you can use to navigate to other user websites belonging to the same webring.&lt;/p&gt;
&lt;p&gt;Others, such as &lt;a href=&#34;https://tanelorn.city&#34; target=&#34;_blank&#34; rel=&#34;noopener noreferrer&#34; &gt;tanelorn.city&lt;/a&gt;, are more focused on publishing Gemini content if this is more interesting to you.&lt;/p&gt;
&lt;p&gt;Either way, I’d recommend browsing from &lt;a href=&#34;https://tildeverse.org&#34; target=&#34;_blank&#34; rel=&#34;noopener noreferrer&#34; &gt;tildeverse.org&lt;/a&gt; as a starting point if you’re interested in getting involved. It helps explain some of the concepts and lists some of the Tildeverse &lt;em&gt;member&lt;/em&gt; servers.&lt;/p&gt;
</content:encoded>
    </item>
    
    <item>
      <title>The Hunt for Red October by Tom Clancy</title>
      <link>https://wilw.dev/blog/2021/03/10/red-october/</link>
      <pubDate>Wed, 10 Mar 2021 20:18:00 +0000</pubDate>
      
      <guid>https://wilw.dev/blog/2021/03/10/red-october/</guid>
      
        <category>100daystooffload</category>
      
        <category>book</category>
      
        <category>opinion</category>
      
      
      <content:encoded>&lt;p&gt;I recently finished reading &lt;a href=&#34;https://www.goodreads.com/book/show/19691.The_Hunt_for_Red_October&#34; target=&#34;_blank&#34; rel=&#34;noopener noreferrer&#34; &gt;The Hunt for Red October&lt;/a&gt; by &lt;a href=&#34;https://www.goodreads.com/author/show/3892.Tom_Clancy&#34; target=&#34;_blank&#34; rel=&#34;noopener noreferrer&#34; &gt;Tom Clancy&lt;/a&gt;.&lt;/p&gt;
&lt;p&gt;&lt;img src=&#34;https://wilw.dev/media/blog/red-october.jpg&#34; alt=&#34;Book cover for The Hunt for Red October&#34;&gt;&lt;/p&gt;
&lt;p&gt;This genre of novel (sort of military thriller fiction) is not usual for me and this is the first Clancy book I have read. That being said, the book has been on my “to-read” list for a fair amount of time and so I am glad I got round to reading it.&lt;/p&gt;
&lt;p&gt;I also hadn’t seen &lt;a href=&#34;https://www.imdb.com/title/tt0099810&#34; target=&#34;_blank&#34; rel=&#34;noopener noreferrer&#34; &gt;the movie&lt;/a&gt; (starring Sean Connery and Alec Baldwin) by the time I read it and so I didn’t have any pre-perceived ideas about the story and could read afresh.&lt;/p&gt;
&lt;p&gt;Side note: I have now since watched the movie, and whilst the core plot is mostly the same there are a lot of differing details throughout (in terms of both angle and storyline), and so I can certainly recommend both media if you’ve previously seen one or the other or neither.&lt;/p&gt;
&lt;p&gt;In general, I very much enjoyed the book. It was an exciting read from start to finish, with interesting characters, relationships and story arcs. I was fascinated by all of the technical detail and also felt that it helped explain and justify many of the core concepts and features of the story. The character development was good, and you quickly build a connection with many of the different people involved.&lt;/p&gt;
&lt;p&gt;Though I do not think this a fault of the author (I imagine the work is an accurate reflection given the time of the setting), I would hope that if it were written in modern times there would be improved gender diversity and more female representation in the novel - as it is I do not remember there being a single female character (aside from mentioning wives and family members who do not appear in the story directly).&lt;/p&gt;
&lt;p&gt;Either way, I can certainly recommend the book to others who also enjoy an exciting story and lots of technical detail. I thought the run-up to the ending was great and I am definitely intrigued to further my reading in this genre.&lt;/p&gt;
</content:encoded>
    </item>
    
    <item>
      <title>Thoughts on minimalism, and what happens when I get mail</title>
      <link>https://wilw.dev/blog/2021/03/08/getting-mail/</link>
      <pubDate>Mon, 08 Mar 2021 19:23:00 +0000</pubDate>
      
      <guid>https://wilw.dev/blog/2021/03/08/getting-mail/</guid>
      
        <category>100daystooffload</category>
      
        <category>life</category>
      
        <category>technology</category>
      
      
      <content:encoded>&lt;h2 id=&#34;minimising-possessions&#34;&gt;Minimising possessions&lt;/h2&gt;
&lt;p&gt;Like many people I these days try and live a minimal life when it comes to possessions. Having more &lt;em&gt;stuff&lt;/em&gt; means there is a greater level of responsibility required to look after it. I love the principles involved in “owning less”.&lt;/p&gt;
&lt;p&gt;Although I am in a very different situation to &lt;a href=&#34;https://levels.io&#34; target=&#34;_blank&#34; rel=&#34;noopener noreferrer&#34; &gt;Pieter Levels&lt;/a&gt;, I find the ideas behind his &lt;a href=&#34;https://levels.io/the-100-thing-challenge&#34; target=&#34;_blank&#34; rel=&#34;noopener noreferrer&#34; &gt;100 Thing Challenge&lt;/a&gt; (and &lt;a href=&#34;https://levels.io/tag/minimalism&#34; target=&#34;_blank&#34; rel=&#34;noopener noreferrer&#34; &gt;other related pieces&lt;/a&gt;) to be inspiring.&lt;/p&gt;
&lt;p&gt;Although my home contains items that are technically mine - furniture, kitchenware, decorations, etc. - I consider these as belonging to the &lt;em&gt;house&lt;/em&gt; itself rather than as my personal belongings. Personal items are essentially the things I can fit into my backpack and are things I actually &lt;em&gt;need&lt;/em&gt; on a daily or weekly basis: my laptop, my phone, some of my clothes, my toothbrush, passport, and a few other smaller items.&lt;/p&gt;
&lt;p&gt;Non-essential things - although a “luxury” - are also a &lt;em&gt;liability&lt;/em&gt; (and an anchor).&lt;/p&gt;
&lt;p&gt;This also helps to keep emotional attachment out of ownership. I know that if I were to lose or break my phone, I could get another and continue on as before. The main concepts here for me are &lt;em&gt;portability&lt;/em&gt; and &lt;em&gt;replaceability&lt;/em&gt;.&lt;/p&gt;
&lt;p&gt;I consider my data and communications to be personal belongings, too. For example, emails I’ve sent and received, documents, images, and so on. Since these are all digital, I can just stick them on my Nextcloud (or &lt;a href=&#34;https://wilw.dev/blog/2021/02/24/google-photos-pcloud&#34;&gt;pCloud&lt;/a&gt;), and I can access them any time through my phone or laptop.&lt;/p&gt;
&lt;p&gt;I also strive for digital minimalism too, where possible. However, since this data storage methodology is scalable and I can keep things very organised I don’t mind holding onto data and documents should they be useful in the future. Even with thousands of stored documents, the collection is still &lt;em&gt;portable&lt;/em&gt; and it fits with my model.&lt;/p&gt;
&lt;h2 id=&#34;mail-and-paperwork&#34;&gt;Mail and paperwork&lt;/h2&gt;
&lt;p&gt;Many of the world’s organisations - including insurance companies, banks, lawyers, and public services - still love doing business with physical documents and through physical mail. Also, these are typically the types of documents you are supposed to keep hold of for long periods of time for the purposes of financial records, insurance certification, and so on. Over time this paperwork builds up and quickly becomes disorganised.&lt;/p&gt;
&lt;p&gt;Some people keep boxes or filing cabinets of documents and mail. This turns into something else to be responsible for. It’s not portable (in the “backpack” idea mentioned earlier) or replaceable. If there was a fire it would be lost, and if moving home it’s something else to “worry” about.&lt;/p&gt;
&lt;p&gt;Until a couple of years ago, I kept documents in ring-binders. My process would include holepunching documents (retro, I know), finding the section of the ringbinder most appropriate for that document, placing the document, and then putting the ringbinders back on the shelf.&lt;/p&gt;
&lt;p&gt;I had years’ worth of utility bills, insurance documents, bank statements, pay-slips, and more that I would need to bring with me whenever I moved and always ensure there was a phycial space for them in my life somewhere.&lt;/p&gt;
&lt;p&gt;I began to realise that - for the vast majority of these documents - I would never really need the &lt;em&gt;original&lt;/em&gt; version. Apart from things like my passport and paper certificates containing security features, document &lt;em&gt;copies&lt;/em&gt; would be fine. And since I already had a system for storing digital documents, I could extend this to maintain a more organised (and searcahble) collection of digitised paper documents too.&lt;/p&gt;
&lt;h2 id=&#34;digitising-paperwork&#34;&gt;Digitising paperwork&lt;/h2&gt;
&lt;p&gt;Phone cameras these days are more than capable of creating high-quality digital replicas of paper documents. There are also many scanner apps and software available to make this easier.&lt;/p&gt;
&lt;p&gt;I personally use &lt;a href=&#34;https://apps.apple.com/app/apple-store/id333710667&#34; target=&#34;_blank&#34; rel=&#34;noopener noreferrer&#34; &gt;Scanner Pro&lt;/a&gt; on my iPhone, which is very useful. It automatically detects paper edges (even documents with weird dimensions) and straightens the image sensibly too. It also has settings to help configure further; for example, I only need greyscale copies and not the highest resolution - both of these factors help decrease the size of the eventual file.&lt;/p&gt;
&lt;p&gt;The official iOS &lt;a href=&#34;https://apps.apple.com/us/app/files/id1232058109&#34; target=&#34;_blank&#34; rel=&#34;noopener noreferrer&#34; &gt;Files app&lt;/a&gt; also has a “Scan Documents” feature, which looks pretty good. I’ve not used this extensively myself yet.&lt;/p&gt;
&lt;p&gt;After downloading the scanner app, I went through my ring-binders and piled up all the documents to throw out - stuff I just didn’t need any record of but had, for some reason, kept anyway. I then went through each remaining section in turn and scanned each document in - storing each PDF to my Nextcloud.&lt;/p&gt;
&lt;p&gt;The process was surprisingly quick and by the end I had a nicely organised collection of files on Nextcloud and a large pile of paper documents I could throw out. As I mentioned earlier, about the only physical things I &lt;em&gt;did&lt;/em&gt; keep were certificates, my passport, and a handful of other items.&lt;/p&gt;
&lt;p&gt;It was a weirdly therapeutic exercise!&lt;/p&gt;
&lt;h2 id=&#34;my-process-now&#34;&gt;My process now&lt;/h2&gt;
&lt;p&gt;Jumping back to the present and my more minimalism-focused self, I am now very strict about what paperwork I keep. In fact, I don’t think I’ve kept hold of a physical document that I’ve received in the last year (and probably longer).&lt;/p&gt;
&lt;p&gt;I have a simple process:&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;I receive the document/paperwork and open it;&lt;/li&gt;
&lt;li&gt;I use my phone to scan the document;&lt;/li&gt;
&lt;li&gt;I sync the file to an &lt;code&gt;0 Unfiled&lt;/code&gt; directory on my Nextcloud, titled by date, sender, and short subject (e.g. &lt;code&gt;2021-03-02_BritishGas_Statement.pdf&lt;/code&gt;);&lt;/li&gt;
&lt;li&gt;I throw the document out (shredding first if sensitive);&lt;/li&gt;
&lt;li&gt;If the paperwork requires action, I either do so immediately or set a reminder to do so;&lt;/li&gt;
&lt;li&gt;Once a month or so I go through my &lt;code&gt;0 Unfiled&lt;/code&gt; directory and categorise properly according to my personal filesystem.&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;I use a “holding” directory (&lt;code&gt;0 Unfiled&lt;/code&gt;) to make the process quicker (for example, if there are several documents to scan) and it ensures I have actually actioned the files once I come round to organising them later. I use a &lt;code&gt;0&lt;/code&gt; at the start of the directory name so that it sits at the top of my filesystem root in order to improve efficiency (and I try and use the &lt;a href=&#34;https://johnnydecimal.com&#34; target=&#34;_blank&#34; rel=&#34;noopener noreferrer&#34; &gt;Johnny.Decimal&lt;/a&gt; concepts as much as possible).&lt;/p&gt;
&lt;p&gt;I also use the holding directory for other important documents - such as email attachments I want to include in this system. To me, it doesn’t matter which medium was used to receive the document: it’s all just data to be categorised and stored.&lt;/p&gt;
&lt;p&gt;It’s a satisfying process. I now feel more organised, I can easily find a particular document - even from several years ago - without needing to trawl through piles of paper; I can ensure &lt;em&gt;longevity&lt;/em&gt; and &lt;em&gt;integrity&lt;/em&gt; of the data (i.e. it can’t get torn or damaged); I can back the collection up with added &lt;em&gt;redundancy&lt;/em&gt;; and I can easily view and share the documents from anywhere.&lt;/p&gt;
&lt;p&gt;If you currently keep lots of paper records and are interested in minimising your physical footprint then I can recommend trying a similar process yourself.&lt;/p&gt;
</content:encoded>
    </item>
    
    <item>
      <title>Easily set up discoverable RSS feeds on a Gatsby website</title>
      <link>https://wilw.dev/blog/2021/03/04/gatsby-rss/</link>
      <pubDate>Thu, 04 Mar 2021 22:17:00 +0000</pubDate>
      
      <guid>https://wilw.dev/blog/2021/03/04/gatsby-rss/</guid>
      
        <category>100daystooffload</category>
      
        <category>technology</category>
      
        <category>javascript</category>
      
      
      <content:encoded>&lt;p&gt;RSS has had a &lt;a href=&#34;https://wilw.dev/blog/2021/02/03/rss-rise-fall-rise&#34;&gt;bit of a resurgence&lt;/a&gt; for personal websites and blogs in recent years, especially with the growing adoption of &lt;a href=&#34;https://ar.al/2020/08/07/what-is-the-small-web&#34; target=&#34;_blank&#34; rel=&#34;noopener noreferrer&#34; &gt;Small Web&lt;/a&gt; and &lt;a href=&#34;https://indieweb.org&#34; target=&#34;_blank&#34; rel=&#34;noopener noreferrer&#34; &gt;IndieWeb&lt;/a&gt; ideologies.&lt;/p&gt;
&lt;p&gt;Many static site generators - including &lt;a href=&#34;https://gohugo.io&#34; target=&#34;_blank&#34; rel=&#34;noopener noreferrer&#34; &gt;Hugo&lt;/a&gt;, &lt;a href=&#34;https://jekyllrb.com&#34; target=&#34;_blank&#34; rel=&#34;noopener noreferrer&#34; &gt;Jekyll&lt;/a&gt;, and &lt;a href=&#34;https://www.11ty.dev&#34; target=&#34;_blank&#34; rel=&#34;noopener noreferrer&#34; &gt;Eleventy&lt;/a&gt; - can easily support the automatic generation of RSS feeds at build time (either directly, or through plugins).&lt;/p&gt;
&lt;p&gt;The same is true for &lt;a href=&#34;https://www.gatsbyjs.com&#34; target=&#34;_blank&#34; rel=&#34;noopener noreferrer&#34; &gt;Gatsby&lt;/a&gt; - the framework currently used to build this static website - and the good news is that setting up one feed, or multiple ones for different categories, only takes a few minutes.&lt;/p&gt;
&lt;h2 id=&#34;your-gatsby-blog-structure&#34;&gt;Your Gatsby blog structure&lt;/h2&gt;
&lt;p&gt;This article talks about RSS feeds for blogs (a typical use-case), but is also relevant for other notes, podcasts, or anything else that is published periodically to your Gatsby site.&lt;/p&gt;
&lt;p&gt;In Gatsby, the typical blog set-up involves the blog entries in markdown format, and a &lt;a href=&#34;https://www.gatsbyjs.com/docs/tutorial/part-seven&#34; target=&#34;_blank&#34; rel=&#34;noopener noreferrer&#34; &gt;template “page”&lt;/a&gt;, which is used to render the markdown blog posts.&lt;/p&gt;
&lt;p&gt;You’ll also probably have a “blog” page which lists or paginates your posts for visitors to find them, and a &lt;code&gt;createPages&lt;/code&gt; function in your &lt;code&gt;gatsby-node.js&lt;/code&gt; that generates the pages from the template and markdown.&lt;/p&gt;
&lt;p&gt;All this sounds way more complicated than it is in practice, and there are lots of &lt;a href=&#34;https://blog.logrocket.com/creating-a-gatsby-blog-from-scratch&#34; target=&#34;_blank&#34; rel=&#34;noopener noreferrer&#34; &gt;guides available&lt;/a&gt; to help set this up.&lt;/p&gt;
&lt;p&gt;At the very least, this article assumes you have blog posts written in a directory containing markdown for each post similar to the following:&lt;/p&gt;
&lt;div class=&#34;highlight&#34;&gt;&lt;pre tabindex=&#34;0&#34; style=&#34;background-color:#f0f3f3;-moz-tab-size:2;-o-tab-size:2;tab-size:2;&#34;&gt;&lt;code class=&#34;language-yaml&#34; data-lang=&#34;yaml&#34;&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;&lt;span style=&#34;color:#0cf;font-weight:bold&#34;&gt;---&lt;/span&gt;&lt;span style=&#34;color:#bbb&#34;&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;&lt;span style=&#34;color:#bbb&#34;&gt;&lt;/span&gt;&lt;span style=&#34;color:#309;font-weight:bold&#34;&gt;date&lt;/span&gt;:&lt;span style=&#34;color:#bbb&#34;&gt; &lt;/span&gt;&lt;span style=&#34;color:#c30&#34;&gt;&#34;2021-03-04T22:17:00Z&#34;&lt;/span&gt;&lt;span style=&#34;color:#bbb&#34;&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;&lt;span style=&#34;color:#bbb&#34;&gt;&lt;/span&gt;&lt;span style=&#34;color:#309;font-weight:bold&#34;&gt;title&lt;/span&gt;:&lt;span style=&#34;color:#bbb&#34;&gt; &lt;/span&gt;&lt;span style=&#34;color:#c30&#34;&gt;&#34;Easily set up discoverable RSS feeds on a Gatsby website&#34;&lt;/span&gt;&lt;span style=&#34;color:#bbb&#34;&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;&lt;span style=&#34;color:#bbb&#34;&gt;&lt;/span&gt;&lt;span style=&#34;color:#309;font-weight:bold&#34;&gt;description&lt;/span&gt;:&lt;span style=&#34;color:#bbb&#34;&gt; &lt;/span&gt;&lt;span style=&#34;color:#c30&#34;&gt;&#34;How to set up multiple discoverable RSS feeds for your static Gatsby website.&#34;&lt;/span&gt;&lt;span style=&#34;color:#bbb&#34;&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;&lt;span style=&#34;color:#bbb&#34;&gt;&lt;/span&gt;&lt;span style=&#34;color:#309;font-weight:bold&#34;&gt;tags&lt;/span&gt;:&lt;span style=&#34;color:#bbb&#34;&gt; &lt;/span&gt;[100daystooffload, technology, javascript]&lt;span style=&#34;color:#bbb&#34;&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;&lt;span style=&#34;color:#bbb&#34;&gt;&lt;/span&gt;&lt;span style=&#34;color:#0cf;font-weight:bold&#34;&gt;---&lt;/span&gt;&lt;span style=&#34;color:#bbb&#34;&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;&lt;span style=&#34;color:#bbb&#34;&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;&lt;span style=&#34;color:#bbb&#34;&gt;&lt;/span&gt;The post content starts here...&lt;span style=&#34;color:#bbb&#34;&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;The metadata (frontmatter) doesn’t need to be exactly as shown, but having useful metadata (e.g. tags) in-place helps make your feeds richer.&lt;/p&gt;
&lt;h2 id=&#34;creating-your-feeds&#34;&gt;Creating your feeds&lt;/h2&gt;
&lt;p&gt;To create the feeds, we’ll use a Gatsby plugin called &lt;a href=&#34;https://www.gatsbyjs.com/plugins/gatsby-plugin-feed&#34; target=&#34;_blank&#34; rel=&#34;noopener noreferrer&#34; &gt;&lt;code&gt;gatsby-plugin-feed&lt;/code&gt;&lt;/a&gt;, which will do most of the heavy-lifting for us (as long as you have a blog in place structured similarly to the way described above).&lt;/p&gt;
&lt;p&gt;First off, add the plugin as a dependency: &lt;code&gt;yarn add gatsby-plugin-feed&lt;/code&gt;. I also recommend installing &lt;code&gt;moment&lt;/code&gt; to help with formatting dates for the feed (as we’ll see later): &lt;code&gt;yarn add moment&lt;/code&gt;.&lt;/p&gt;
&lt;p&gt;Next, you’ll need to create some code in &lt;code&gt;gatsby-config.js&lt;/code&gt;. If you have a blog already then you likely already have content in this file (e.g. &lt;code&gt;gatsby-source-filesystem&lt;/code&gt; configuration). Your file probably looks a little like the following:&lt;/p&gt;
&lt;div class=&#34;highlight&#34;&gt;&lt;pre tabindex=&#34;0&#34; style=&#34;background-color:#f0f3f3;-moz-tab-size:2;-o-tab-size:2;tab-size:2;&#34;&gt;&lt;code class=&#34;language-javascript&#34; data-lang=&#34;javascript&#34;&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;module.exports &lt;span style=&#34;color:#555&#34;&gt;=&lt;/span&gt; {
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;  siteMetadata&lt;span style=&#34;color:#555&#34;&gt;:&lt;/span&gt; {
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;    title&lt;span style=&#34;color:#555&#34;&gt;:&lt;/span&gt; &lt;span style=&#34;color:#c30&#34;&gt;&#39;My Cool Website&#39;&lt;/span&gt;,
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;    siteUrl&lt;span style=&#34;color:#555&#34;&gt;:&lt;/span&gt; &lt;span style=&#34;color:#c30&#34;&gt;&#39;https://my.cool.website&#39;&lt;/span&gt;,
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;  },
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;  plugins&lt;span style=&#34;color:#555&#34;&gt;:&lt;/span&gt; [
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;    {
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;      resolve&lt;span style=&#34;color:#555&#34;&gt;:&lt;/span&gt; &lt;span style=&#34;color:#c30&#34;&gt;&#39;gatsby-source-filesystem&#39;&lt;/span&gt;,
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;      options&lt;span style=&#34;color:#555&#34;&gt;:&lt;/span&gt; { ... },
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;    },
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;    &lt;span style=&#34;color:#c30&#34;&gt;&#39;gatsby-plugin-react-helmet&#39;&lt;/span&gt;,
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;  ],
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;};
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;Along with any other plugins you may have.&lt;/p&gt;
&lt;p&gt;To create the feed we’ll make use of a GraphQL query, and a function which will create a feed object. If we define these separately (as below), it will give us more flexibility later.&lt;/p&gt;
&lt;p&gt;In the same file (&lt;code&gt;gatsby-config.js&lt;/code&gt;), at the top, first &lt;code&gt;require&lt;/code&gt; the &lt;code&gt;moment&lt;/code&gt; library we installed earlier, define the query we’ll use, and a function to create a feed object:&lt;/p&gt;
&lt;div class=&#34;highlight&#34;&gt;&lt;pre tabindex=&#34;0&#34; style=&#34;background-color:#f0f3f3;-moz-tab-size:2;-o-tab-size:2;tab-size:2;&#34;&gt;&lt;code class=&#34;language-javascript&#34; data-lang=&#34;javascript&#34;&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;&lt;span style=&#34;color:#069;font-weight:bold&#34;&gt;const&lt;/span&gt; moment &lt;span style=&#34;color:#555&#34;&gt;=&lt;/span&gt; require(&lt;span style=&#34;color:#c30&#34;&gt;&#39;moment&#39;&lt;/span&gt;);
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;&lt;span style=&#34;color:#09f;font-style:italic&#34;&gt;// Query for all blog posts ordered by filename (i.e. date) descending
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;&lt;span style=&#34;color:#09f;font-style:italic&#34;&gt;&lt;/span&gt;&lt;span style=&#34;color:#069;font-weight:bold&#34;&gt;const&lt;/span&gt; rssPostQuery &lt;span style=&#34;color:#555&#34;&gt;=&lt;/span&gt; &lt;span style=&#34;color:#c30&#34;&gt;`
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;&lt;span style=&#34;color:#c30&#34;&gt;{
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;&lt;span style=&#34;color:#c30&#34;&gt;  allMarkdownRemark(
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;&lt;span style=&#34;color:#c30&#34;&gt;    sort: { order: DESC, fields: [fileAbsolutePath] },
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;&lt;span style=&#34;color:#c30&#34;&gt;    filter: { fields: { slug: { regex: &#34;/blog/&#34; } } }
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;&lt;span style=&#34;color:#c30&#34;&gt;  ) {
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;&lt;span style=&#34;color:#c30&#34;&gt;    edges {
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;&lt;span style=&#34;color:#c30&#34;&gt;      node {
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;&lt;span style=&#34;color:#c30&#34;&gt;        html
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;&lt;span style=&#34;color:#c30&#34;&gt;        fields { slug }
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;&lt;span style=&#34;color:#c30&#34;&gt;        frontmatter {
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;&lt;span style=&#34;color:#c30&#34;&gt;          title
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;&lt;span style=&#34;color:#c30&#34;&gt;          description
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;&lt;span style=&#34;color:#c30&#34;&gt;          date
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;&lt;span style=&#34;color:#c30&#34;&gt;          tags
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;&lt;span style=&#34;color:#c30&#34;&gt;        }
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;&lt;span style=&#34;color:#c30&#34;&gt;      }
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;&lt;span style=&#34;color:#c30&#34;&gt;    }
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;&lt;span style=&#34;color:#c30&#34;&gt;  }
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;&lt;span style=&#34;color:#c30&#34;&gt;}
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;&lt;span style=&#34;color:#c30&#34;&gt;`&lt;/span&gt;;
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;&lt;span style=&#34;color:#069;font-weight:bold&#34;&gt;const&lt;/span&gt; createRssPost &lt;span style=&#34;color:#555&#34;&gt;=&lt;/span&gt; (edge, site) =&gt; {
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;  &lt;span style=&#34;color:#069;font-weight:bold&#34;&gt;const&lt;/span&gt; { node } &lt;span style=&#34;color:#555&#34;&gt;=&lt;/span&gt; edge;
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;  &lt;span style=&#34;color:#069;font-weight:bold&#34;&gt;const&lt;/span&gt; { slug } &lt;span style=&#34;color:#555&#34;&gt;=&lt;/span&gt; node.fields;
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;  &lt;span style=&#34;color:#069;font-weight:bold&#34;&gt;return&lt;/span&gt; &lt;span style=&#34;color:#366&#34;&gt;Object&lt;/span&gt;.assign({}, edge.node.frontmatter, {
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;    description&lt;span style=&#34;color:#555&#34;&gt;:&lt;/span&gt; edge.node.description,
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;    date&lt;span style=&#34;color:#555&#34;&gt;:&lt;/span&gt; moment.utc(&lt;span style=&#34;color:#c30&#34;&gt;`&lt;/span&gt;&lt;span style=&#34;color:#a00&#34;&gt;${&lt;/span&gt;node.frontmatter.date&lt;span style=&#34;color:#a00&#34;&gt;}&lt;/span&gt;&lt;span style=&#34;color:#c30&#34;&gt;`&lt;/span&gt;, &lt;span style=&#34;color:#c30&#34;&gt;&#39;YYYY/MM/DDTHH:mmZ&#39;&lt;/span&gt;).format(),
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;    url&lt;span style=&#34;color:#555&#34;&gt;:&lt;/span&gt; site.siteMetadata.siteUrl &lt;span style=&#34;color:#555&#34;&gt;&#43;&lt;/span&gt; slug,
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;    guid&lt;span style=&#34;color:#555&#34;&gt;:&lt;/span&gt; site.siteMetadata.siteUrl &lt;span style=&#34;color:#555&#34;&gt;&#43;&lt;/span&gt; slug,
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;    custom_elements&lt;span style=&#34;color:#555&#34;&gt;:&lt;/span&gt; [{ &lt;span style=&#34;color:#c30&#34;&gt;&#34;content:encoded&#34;&lt;/span&gt;&lt;span style=&#34;color:#555&#34;&gt;:&lt;/span&gt; edge.node.html }],
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;  });;
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;};
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;The &lt;code&gt;rssPostQuery&lt;/code&gt; assumes your blog posts are rendered at &lt;code&gt;/blog/filename&lt;/code&gt; in your built site. If not, then just change this value in the regex. Likewise, the &lt;code&gt;createRssPost&lt;/code&gt; function assumes the dates in the frontmatter of your posts are formatted like &lt;code&gt;YYYY/MM/DDTHH:mmZ&lt;/code&gt; - if not, just change this string to match your own format (I use UTC here as we’re dealing with global audiences!).&lt;/p&gt;
&lt;p&gt;Essentially, the GraphQL query string returns all markdown files ordered by descending filename (I title my blog posts by date, so this gives a reverse chronological ordering of posts, with the newest first), and gives us the post content, slug (“path”), and selected fields from the posts’ frontmatters.&lt;/p&gt;
&lt;p&gt;We use a regex in the query to discern between different types of markdown files. For example, you may have a collection of notes - also written in markdown - which we want to ignore for the purposes of creating an RSS feed for &lt;em&gt;just&lt;/em&gt; blog posts.&lt;/p&gt;
&lt;p&gt;The &lt;code&gt;createRssPost&lt;/code&gt; function (which we’ll call later), accepts a markdown file (&lt;code&gt;edge&lt;/code&gt;) and information about the website (&lt;code&gt;site&lt;/code&gt;), and returns a fresh object representing this information to be eventually embedded in the feed.&lt;/p&gt;
&lt;p&gt;The &lt;code&gt;guid&lt;/code&gt; field is a globally-unique ID for this post on your blog and reader software will use this to, for example, determine if the user has already seen the post and should mark it as “read”. Since all of my posts have a unique path (“slug”), I just use this for the ID.&lt;/p&gt;
&lt;p&gt;Finally, we need to add a section to our &lt;code&gt;plugins&lt;/code&gt; array to tell &lt;code&gt;gatsby-plugin-feed&lt;/code&gt; how to build our feed using the query and function we created above. In the same file, make the following changes:&lt;/p&gt;
&lt;div class=&#34;highlight&#34;&gt;&lt;pre tabindex=&#34;0&#34; style=&#34;background-color:#f0f3f3;-moz-tab-size:2;-o-tab-size:2;tab-size:2;&#34;&gt;&lt;code class=&#34;language-javascript&#34; data-lang=&#34;javascript&#34;&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;module.exports &lt;span style=&#34;color:#555&#34;&gt;=&lt;/span&gt; {
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;  siteMetadata&lt;span style=&#34;color:#555&#34;&gt;:&lt;/span&gt; { ... }, &lt;span style=&#34;color:#09f;font-style:italic&#34;&gt;// omitted for brevity
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;&lt;span style=&#34;color:#09f;font-style:italic&#34;&gt;&lt;/span&gt;  plugins&lt;span style=&#34;color:#555&#34;&gt;:&lt;/span&gt; [
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;    {
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;      resolve&lt;span style=&#34;color:#555&#34;&gt;:&lt;/span&gt; &lt;span style=&#34;color:#c30&#34;&gt;&#39;gatsby-source-filesystem&#39;&lt;/span&gt;,
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;      options&lt;span style=&#34;color:#555&#34;&gt;:&lt;/span&gt; { ... }, &lt;span style=&#34;color:#09f;font-style:italic&#34;&gt;// omitted for brevity
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;&lt;span style=&#34;color:#09f;font-style:italic&#34;&gt;&lt;/span&gt;    },
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;    { &lt;span style=&#34;color:#09f;font-style:italic&#34;&gt;// Add this object to your &#34;plugins&#34; array:
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;&lt;span style=&#34;color:#09f;font-style:italic&#34;&gt;&lt;/span&gt;      resolve&lt;span style=&#34;color:#555&#34;&gt;:&lt;/span&gt; &lt;span style=&#34;color:#c30&#34;&gt;&#39;gatsby-plugin-feed&#39;&lt;/span&gt;,
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;      options&lt;span style=&#34;color:#555&#34;&gt;:&lt;/span&gt; {
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;        feeds&lt;span style=&#34;color:#555&#34;&gt;:&lt;/span&gt; [
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;          {
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;            serialize&lt;span style=&#34;color:#555&#34;&gt;:&lt;/span&gt; ({ query&lt;span style=&#34;color:#555&#34;&gt;:&lt;/span&gt; { site, allMarkdownRemark } }) =&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;              allMarkdownRemark.edges.map(e =&gt; createRssPost(e, site)),
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;            query&lt;span style=&#34;color:#555&#34;&gt;:&lt;/span&gt; rssPostQuery,
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;            output&lt;span style=&#34;color:#555&#34;&gt;:&lt;/span&gt; &lt;span style=&#34;color:#c30&#34;&gt;&#39;/rss.xml;,
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;&lt;span style=&#34;color:#c30&#34;&gt;            title: &#39;&lt;/span&gt;My Cool Blog&lt;span style=&#34;color:#c30&#34;&gt;&#39;,
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;&lt;span style=&#34;color:#c30&#34;&gt;            description: &#39;&lt;/span&gt;All &lt;span style=&#34;color:#069;font-weight:bold&#34;&gt;of&lt;/span&gt; my blog posts&lt;span style=&#34;color:#a00;background-color:#faa&#34;&gt;&#39;&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;          },
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;        ],
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;      },
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;    },
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;    ...
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;  ],
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;};
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;The &lt;code&gt;gatsby-plugin-feed&lt;/code&gt; plugin only runs when the site is actually &lt;em&gt;built&lt;/em&gt;. If you have your Gatsby site running locally, just run &lt;code&gt;gatsby build&lt;/code&gt; in a separate Terminal window and then navigate to &lt;code&gt;/rss.xml&lt;/code&gt; on your local development website to view the feed.&lt;/p&gt;
&lt;h2 id=&#34;creating-multiple-feeds&#34;&gt;Creating multiple feeds&lt;/h2&gt;
&lt;p&gt;The example configuration in the previous section creates a single feed containing all blog posts.&lt;/p&gt;
&lt;p&gt;However, you may have noticed that the &lt;code&gt;feeds&lt;/code&gt; attribute is an array; this means that the plugin can be used to create multiple feeds. I do exactly that on &lt;a href=&#34;https://wilw.dev/feeds&#34;&gt;this website&lt;/a&gt;: I have different feeds for different audiences (e.g. for technology, life, books, etc.).&lt;/p&gt;
&lt;p&gt;Since we’ve already broken our code out into a separate query and function, it is easy to add new feeds by &lt;code&gt;filter&lt;/code&gt;ing on the markdown edges before passing them to &lt;code&gt;map&lt;/code&gt; in the &lt;code&gt;serialize&lt;/code&gt; function.&lt;/p&gt;
&lt;p&gt;If you modify the same file again (&lt;code&gt;gatsby-config.js&lt;/code&gt;), you can create a feed for all of your posts that contain a tag named “technology” as follows:&lt;/p&gt;
&lt;div class=&#34;highlight&#34;&gt;&lt;pre tabindex=&#34;0&#34; style=&#34;background-color:#f0f3f3;-moz-tab-size:2;-o-tab-size:2;tab-size:2;&#34;&gt;&lt;code class=&#34;language-javascript&#34; data-lang=&#34;javascript&#34;&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;  ... &lt;span style=&#34;color:#09f;font-style:italic&#34;&gt;// omitted for brevity
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;&lt;span style=&#34;color:#09f;font-style:italic&#34;&gt;&lt;/span&gt;    {
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;      resolve&lt;span style=&#34;color:#555&#34;&gt;:&lt;/span&gt; &lt;span style=&#34;color:#c30&#34;&gt;&#39;gatsby-plugin-feed&#39;&lt;/span&gt;,
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;      options&lt;span style=&#34;color:#555&#34;&gt;:&lt;/span&gt; {
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;        feeds&lt;span style=&#34;color:#555&#34;&gt;:&lt;/span&gt; [
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;          { ... }, &lt;span style=&#34;color:#09f;font-style:italic&#34;&gt;// omitted for brevity
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;&lt;span style=&#34;color:#09f;font-style:italic&#34;&gt;&lt;/span&gt;          {
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;            serialize&lt;span style=&#34;color:#555&#34;&gt;:&lt;/span&gt; ({ query&lt;span style=&#34;color:#555&#34;&gt;:&lt;/span&gt; { site, allMarkdownRemark } }) =&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;              allMarkdownRemark.edges.filter(e =&gt; {
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;                &lt;span style=&#34;color:#069;font-weight:bold&#34;&gt;const&lt;/span&gt; tags &lt;span style=&#34;color:#555&#34;&gt;=&lt;/span&gt; e.node.frontmatter.tags;
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;                &lt;span style=&#34;color:#069;font-weight:bold&#34;&gt;return&lt;/span&gt; tags &lt;span style=&#34;color:#555&#34;&gt;&amp;&amp;&lt;/span&gt; tags.length &lt;span style=&#34;color:#555&#34;&gt;&gt;&lt;/span&gt; &lt;span style=&#34;color:#f60&#34;&gt;0&lt;/span&gt; &lt;span style=&#34;color:#555&#34;&gt;&amp;&amp;&lt;/span&gt; tags.indexOf(&lt;span style=&#34;color:#c30&#34;&gt;&#39;technology&#39;&lt;/span&gt;) &lt;span style=&#34;color:#555&#34;&gt;&gt;&lt;/span&gt; &lt;span style=&#34;color:#555&#34;&gt;-&lt;/span&gt;&lt;span style=&#34;color:#f60&#34;&gt;1&lt;/span&gt;;
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;            }).map(e =&gt; createRssPost(e, site)),
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;            query&lt;span style=&#34;color:#555&#34;&gt;:&lt;/span&gt; rssPostQuery,
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;            output&lt;span style=&#34;color:#555&#34;&gt;:&lt;/span&gt; &lt;span style=&#34;color:#c30&#34;&gt;&#39;/technology.xml&#39;&lt;/span&gt;,
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;            title&lt;span style=&#34;color:#555&#34;&gt;:&lt;/span&gt; &lt;span style=&#34;color:#c30&#34;&gt;&#39;My Technology Blog&#39;&lt;/span&gt;,
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;            description&lt;span style=&#34;color:#555&#34;&gt;:&lt;/span&gt; &lt;span style=&#34;color:#c30&#34;&gt;&#39;Posts in my blog tagged with &#34;technology&#34;.&#39;&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;          },
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;        ],
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;      },
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;    },
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;  ...
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;This will create a new feed at &lt;code&gt;/technology.xml&lt;/code&gt; containing these tech posts.&lt;/p&gt;
&lt;p&gt;Since it’s just plain old JavaScript, you can use any of the available information to craft a number of flexible feeds for your visitors to subscribe to. You can then list these feeds on a page on your site, like &lt;a href=&#34;https://wilw.dev/feeds&#34;&gt;this one&lt;/a&gt;.&lt;/p&gt;
&lt;h2 id=&#34;feed-discovery&#34;&gt;Feed discovery&lt;/h2&gt;
&lt;p&gt;The &lt;code&gt;gatsby-plugin-feed&lt;/code&gt; plugin has one more trick up its sleeve: without any extra work it will automatically inject the relevant &lt;code&gt;&lt;link /&gt;&lt;/code&gt; tags to your site’s HTML at build-time to list the feeds that you have configured.&lt;/p&gt;
&lt;p&gt;This means that your visitors just need to add your site’s root URL (e.g. “&lt;a href=&#34;https://my.cool.website&#34; target=&#34;_blank&#34; rel=&#34;noopener noreferrer&#34; &gt;https://my.cool.website&lt;/a&gt;”) into their feed reader and it will suggest the available feeds to them.&lt;/p&gt;
&lt;p&gt;&lt;img src=&#34;https://wilw.dev/media/blog/reeder-feeds.png&#34; alt=&#34;A screenshot showing the Reeder app auto-listing my website&amp;rsquo;s feeds&#34;&gt;&lt;/p&gt;
&lt;p&gt;The image above shows the &lt;a href=&#34;https://www.reederapp.com&#34; target=&#34;_blank&#34; rel=&#34;noopener noreferrer&#34; &gt;Reeder macOS app&lt;/a&gt; automatically listing the available feeds on my website after entering just the root URL for the site. Visitors can then just add the ones they want.&lt;/p&gt;
</content:encoded>
    </item>
    
    <item>
      <title>Making your Python Flask app serverless</title>
      <link>https://wilw.dev/blog/2021/02/28/flask-serverless/</link>
      <pubDate>Sun, 28 Feb 2021 22:05:00 +0000</pubDate>
      
      <guid>https://wilw.dev/blog/2021/02/28/flask-serverless/</guid>
      
        <category>100daystooffload</category>
      
        <category>technology</category>
      
        <category>python</category>
      
      
        <enclosure url="https://wilw.dev/media/blog/header-flask-serverless.png" type="image/png"/>
      
      <content:encoded>&lt;p&gt;Python’s &lt;a href=&#34;https://flask.palletsprojects.com&#34; target=&#34;_blank&#34; rel=&#34;noopener noreferrer&#34; &gt;Flask framework&lt;/a&gt; is an easy and excellent tool for writing web applications. Its in-built features and ecosystem of supporting packages let you create extensible web APIs, handle data and form submissions, render HTML, handle websockets, set-up secure account-management, and much more.&lt;/p&gt;
&lt;p&gt;It’s no wonder the framework is used by individuals, small teams and all the way through to large enterprise applications. A very simple, yet still viable, Flask app with a couple of endpoints looks as follows.&lt;/p&gt;
&lt;div class=&#34;highlight&#34;&gt;&lt;pre tabindex=&#34;0&#34; style=&#34;background-color:#f0f3f3;-moz-tab-size:2;-o-tab-size:2;tab-size:2;&#34;&gt;&lt;code class=&#34;language-python&#34; data-lang=&#34;python&#34;&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;&lt;span style=&#34;color:#069;font-weight:bold&#34;&gt;from&lt;/span&gt; &lt;span style=&#34;color:#0cf;font-weight:bold&#34;&gt;flask&lt;/span&gt; &lt;span style=&#34;color:#069;font-weight:bold&#34;&gt;import&lt;/span&gt; Flask
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;app &lt;span style=&#34;color:#555&#34;&gt;=&lt;/span&gt; Flask(__name__)
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;&lt;span style=&#34;color:#99f&#34;&gt;@app.route&lt;/span&gt;(&lt;span style=&#34;color:#c30&#34;&gt;&#39;/&#39;&lt;/span&gt;)
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;&lt;span style=&#34;color:#069;font-weight:bold&#34;&gt;def&lt;/span&gt; &lt;span style=&#34;color:#c0f&#34;&gt;hello_world&lt;/span&gt;():
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;  &lt;span style=&#34;color:#069;font-weight:bold&#34;&gt;return&lt;/span&gt; &lt;span style=&#34;color:#c30&#34;&gt;&#39;Hello, World!&#39;&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;&lt;span style=&#34;color:#99f&#34;&gt;@app.route&lt;/span&gt;(&lt;span style=&#34;color:#c30&#34;&gt;&#39;/&lt;name&gt;&#39;&lt;/span&gt;)
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;&lt;span style=&#34;color:#069;font-weight:bold&#34;&gt;def&lt;/span&gt; &lt;span style=&#34;color:#c0f&#34;&gt;greet&lt;/span&gt;(name):
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;  &lt;span style=&#34;color:#069;font-weight:bold&#34;&gt;return&lt;/span&gt; &lt;span style=&#34;color:#c30&#34;&gt;&#39;Hello, &#39;&lt;/span&gt; &lt;span style=&#34;color:#555&#34;&gt;&#43;&lt;/span&gt; name 
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;Flask apps like this can easily be deployed to a server (e.g. a VPS) or to an app-delivery service (e.g. &lt;a href=&#34;https://www.heroku.com&#34; target=&#34;_blank&#34; rel=&#34;noopener noreferrer&#34; &gt;Heroku&lt;/a&gt;, &lt;a href=&#34;https://aws.amazon.com/elasticbeanstalk&#34; target=&#34;_blank&#34; rel=&#34;noopener noreferrer&#34; &gt;AWS Elastic Beanstalk&lt;/a&gt;, and &lt;a href=&#34;https://www.digitalocean.com/products/app-platform&#34; target=&#34;_blank&#34; rel=&#34;noopener noreferrer&#34; &gt;Digital Ocean App Platform&lt;/a&gt;). In these scenarios, the server/provider often charges the developer for each hour the app is running. Additionally, as traffic increases or reduces, the provider can automatically scale up and down the resources powering your app in order to meet demand. However this scaling can sometimes be a slow process and also means that the developer is charged even when the app is not being used.&lt;/p&gt;
&lt;p&gt;If you want to set-up your app so that it can automatically scale from 0 to thousands of concurrent users almost instantly, where you are not charged when users aren’t using your app, where it is highly-available (keep up your uptime to meet SLAs), and where there is no server set-up or maintenance required (and there is nothing for bad actors to try and SSH into), then migrating to a more serverless architecture might be of interest to you.&lt;/p&gt;
&lt;p&gt;Also, given that most providers offer a pretty generous free tier for serverless apps, you may not end up paying much at all (up to a few dollars max a month) until you start generating enough traffic.&lt;/p&gt;
&lt;p&gt;&lt;em&gt;Note: in this article I use Flask as an example, however the same should apply to any WSGI-compatible framework, such as Bottle and Django, too.&lt;/em&gt;&lt;/p&gt;
&lt;h2 id=&#34;what-is-a-serverless-web-app&#34;&gt;What is a serverless web app?&lt;/h2&gt;
&lt;p&gt;“Serverless” is the generic term for a family of cloud-based execution models where the developer does not need to worry about provisioning, managing, and maintaining the servers that run their application code. Instead, the developer can focus on writing the application and can rely on the cloud &lt;em&gt;provider&lt;/em&gt; to provision the needed resources and ensure the application is kept highly-available.&lt;/p&gt;
&lt;p&gt;Although services such as Heroku and &lt;a href=&#34;https://www.digitalocean.com/products/app-platform&#34; target=&#34;_blank&#34; rel=&#34;noopener noreferrer&#34; &gt;Digital Ocean App Platform&lt;/a&gt; can be considered “serverless” too (in that there is no server to configure by the developer), I refer more to delivery via &lt;em&gt;function as a service&lt;/em&gt; as the particular serverless model of interest in this article, since this offers the benefits listed at the end of the previous section.&lt;/p&gt;
&lt;p&gt;“Function as a service” (FaaS) - as its name suggests - involves writing &lt;em&gt;functions&lt;/em&gt;, which are deployed to a FaaS provider and can then be &lt;em&gt;invoked&lt;/em&gt;. Such systems are &lt;em&gt;event-driven&lt;/em&gt;, in that the functions are called as a result of a particular event occurring - such as on a periodic schedule (e.g. a cron job) or, in the web application case, an HTTP request.&lt;/p&gt;
&lt;p&gt;There are many FaaS providers, such as &lt;a href=&#34;https://docs.microsoft.com/en-us/azure/azure-functions/functions-overview&#34; target=&#34;_blank&#34; rel=&#34;noopener noreferrer&#34; &gt;Azure Functions&lt;/a&gt;, &lt;a href=&#34;https://cloud.google.com/functions&#34; target=&#34;_blank&#34; rel=&#34;noopener noreferrer&#34; &gt;Google Cloud Functions&lt;/a&gt;, &lt;a href=&#34;https://workers.cloudflare.com&#34; target=&#34;_blank&#34; rel=&#34;noopener noreferrer&#34; &gt;Cloudflare Workers&lt;/a&gt;, and &lt;a href=&#34;https://www.ibm.com/cloud/functions&#34; target=&#34;_blank&#34; rel=&#34;noopener noreferrer&#34; &gt;IBM Cloud Functions&lt;/a&gt;.&lt;/p&gt;
&lt;p&gt;Probably the most famous (and first major) FaaS provider offering is &lt;a href=&#34;https://aws.amazon.com/lambda&#34; target=&#34;_blank&#34; rel=&#34;noopener noreferrer&#34; &gt;AWS Lambda&lt;/a&gt;. In this article I will focus on using Lambda as the tool for deploying Flask apps, but many of the concepts discussed are generic across providers.&lt;/p&gt;
&lt;p&gt;Serverless apps written using AWS Lambda usually also involve &lt;a href=&#34;https://aws.amazon.com/api-gateway/features&#34; target=&#34;_blank&#34; rel=&#34;noopener noreferrer&#34; &gt;Amazon API Gateway&lt;/a&gt;, which handles the HTTP request/response side of things and passes the information through as code to the Lambda function. The &lt;code&gt;event&lt;/code&gt; argument received by the function describes - among other things - the information about the request that can be used to generate an appropriate response, which is then returned by the function.&lt;/p&gt;
&lt;div class=&#34;highlight&#34;&gt;&lt;pre tabindex=&#34;0&#34; style=&#34;background-color:#f0f3f3;-moz-tab-size:2;-o-tab-size:2;tab-size:2;&#34;&gt;&lt;code class=&#34;language-python&#34; data-lang=&#34;python&#34;&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;&lt;span style=&#34;color:#069;font-weight:bold&#34;&gt;import&lt;/span&gt; &lt;span style=&#34;color:#0cf;font-weight:bold&#34;&gt;json&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;        
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;&lt;span style=&#34;color:#069;font-weight:bold&#34;&gt;def&lt;/span&gt; &lt;span style=&#34;color:#c0f&#34;&gt;lambda_handler&lt;/span&gt;(event, context):
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;  name &lt;span style=&#34;color:#555&#34;&gt;=&lt;/span&gt; event[&lt;span style=&#34;color:#c30&#34;&gt;&#39;queryStringParameters&#39;&lt;/span&gt;][&lt;span style=&#34;color:#c30&#34;&gt;&#39;name&#39;&lt;/span&gt;]
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;  &lt;span style=&#34;color:#069;font-weight:bold&#34;&gt;return&lt;/span&gt; {
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;    &lt;span style=&#34;color:#c30&#34;&gt;&#34;statusCode&#34;&lt;/span&gt;: &lt;span style=&#34;color:#f60&#34;&gt;200&lt;/span&gt;,
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;    &lt;span style=&#34;color:#c30&#34;&gt;&#34;headers&#34;&lt;/span&gt;: {&lt;span style=&#34;color:#c30&#34;&gt;&#34;Content-Type&#34;&lt;/span&gt;: &lt;span style=&#34;color:#c30&#34;&gt;&#34;application/json&#34;&lt;/span&gt;},
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;    &lt;span style=&#34;color:#c30&#34;&gt;&#34;body&#34;&lt;/span&gt;: json&lt;span style=&#34;color:#555&#34;&gt;.&lt;/span&gt;dumps({&lt;span style=&#34;color:#c30&#34;&gt;&#34;Hello&#34;&lt;/span&gt;: name})
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;  }
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;As long as your function(s) return a valid object to generate a response from API Gateway, applications on Lambda can use a separate function for each request path and method combination, or use one function for &lt;em&gt;all&lt;/em&gt; invocations from API Gateway and use the &lt;code&gt;event&lt;/code&gt; parameter and code logic to provide the needed actions.&lt;/p&gt;
&lt;p&gt;Either way, this is a different pattern to how Flask structures its functions, requests, and responses. As such, we can’t simply deploy our Flask app as-is to Lambda. I’ll now talk about how we &lt;em&gt;can&lt;/em&gt; do it without too much extra work.&lt;/p&gt;
&lt;h2 id=&#34;using-serverless-framework-to-describe-a-basic-app&#34;&gt;Using Serverless framework to describe a basic app&lt;/h2&gt;
&lt;p&gt;The &lt;a href=&#34;https://www.serverless.com&#34; target=&#34;_blank&#34; rel=&#34;noopener noreferrer&#34; &gt;Serverless framework&lt;/a&gt;, along with its extensive &lt;a href=&#34;https://www.serverless.com/plugins&#34; target=&#34;_blank&#34; rel=&#34;noopener noreferrer&#34; &gt;library of plugins&lt;/a&gt;, is a well-established tool for provisioning serverless applications on a number of providers. It bundles your code and automates the deployment process, making it easy to create a serverless app.&lt;/p&gt;
&lt;p&gt;Configuration of apps deployed using Serverless is done through the &lt;code&gt;serverless.yml&lt;/code&gt; file. The example configuration below would, when deployed, create an API Gateway interface and a Lambda function using the code in &lt;code&gt;app.py&lt;/code&gt;, and would invoke the &lt;code&gt;lambda_handler&lt;/code&gt; function (above) each time a &lt;code&gt;GET&lt;/code&gt; request is made to &lt;code&gt;/hello&lt;/code&gt;:&lt;/p&gt;
&lt;div class=&#34;highlight&#34;&gt;&lt;pre tabindex=&#34;0&#34; style=&#34;background-color:#f0f3f3;-moz-tab-size:2;-o-tab-size:2;tab-size:2;&#34;&gt;&lt;code class=&#34;language-yml&#34; data-lang=&#34;yml&#34;&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;&lt;span style=&#34;color:#309;font-weight:bold&#34;&gt;service&lt;/span&gt;:&lt;span style=&#34;color:#bbb&#34;&gt; &lt;/span&gt;my-hello-app&lt;span style=&#34;color:#bbb&#34;&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;&lt;span style=&#34;color:#bbb&#34;&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;&lt;span style=&#34;color:#bbb&#34;&gt;&lt;/span&gt;&lt;span style=&#34;color:#309;font-weight:bold&#34;&gt;provider&lt;/span&gt;:&lt;span style=&#34;color:#bbb&#34;&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;&lt;span style=&#34;color:#bbb&#34;&gt;  &lt;/span&gt;&lt;span style=&#34;color:#309;font-weight:bold&#34;&gt;name&lt;/span&gt;:&lt;span style=&#34;color:#bbb&#34;&gt; &lt;/span&gt;aws&lt;span style=&#34;color:#bbb&#34;&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;&lt;span style=&#34;color:#bbb&#34;&gt;  &lt;/span&gt;&lt;span style=&#34;color:#309;font-weight:bold&#34;&gt;runtime&lt;/span&gt;:&lt;span style=&#34;color:#bbb&#34;&gt; &lt;/span&gt;python3.8&lt;span style=&#34;color:#bbb&#34;&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;&lt;span style=&#34;color:#bbb&#34;&gt;  &lt;/span&gt;&lt;span style=&#34;color:#309;font-weight:bold&#34;&gt;region&lt;/span&gt;:&lt;span style=&#34;color:#bbb&#34;&gt; &lt;/span&gt;eu-west-1&lt;span style=&#34;color:#bbb&#34;&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;&lt;span style=&#34;color:#bbb&#34;&gt;  &lt;/span&gt;&lt;span style=&#34;color:#309;font-weight:bold&#34;&gt;memorySize&lt;/span&gt;:&lt;span style=&#34;color:#bbb&#34;&gt; &lt;/span&gt;&lt;span style=&#34;color:#f60&#34;&gt;512&lt;/span&gt;&lt;span style=&#34;color:#bbb&#34;&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;&lt;span style=&#34;color:#bbb&#34;&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;&lt;span style=&#34;color:#bbb&#34;&gt;&lt;/span&gt;&lt;span style=&#34;color:#309;font-weight:bold&#34;&gt;functions&lt;/span&gt;:&lt;span style=&#34;color:#bbb&#34;&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;&lt;span style=&#34;color:#bbb&#34;&gt;  &lt;/span&gt;&lt;span style=&#34;color:#309;font-weight:bold&#34;&gt;hello&lt;/span&gt;:&lt;span style=&#34;color:#bbb&#34;&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;&lt;span style=&#34;color:#bbb&#34;&gt;    &lt;/span&gt;&lt;span style=&#34;color:#309;font-weight:bold&#34;&gt;handler&lt;/span&gt;:&lt;span style=&#34;color:#bbb&#34;&gt; &lt;/span&gt;app.lambda_handler&lt;span style=&#34;color:#bbb&#34;&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;&lt;span style=&#34;color:#bbb&#34;&gt;    &lt;/span&gt;&lt;span style=&#34;color:#309;font-weight:bold&#34;&gt;events&lt;/span&gt;:&lt;span style=&#34;color:#bbb&#34;&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;&lt;span style=&#34;color:#bbb&#34;&gt;      &lt;/span&gt;- &lt;span style=&#34;color:#309;font-weight:bold&#34;&gt;http&lt;/span&gt;:&lt;span style=&#34;color:#bbb&#34;&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;&lt;span style=&#34;color:#bbb&#34;&gt;          &lt;/span&gt;&lt;span style=&#34;color:#309;font-weight:bold&#34;&gt;path&lt;/span&gt;:&lt;span style=&#34;color:#bbb&#34;&gt; &lt;/span&gt;hello&lt;span style=&#34;color:#bbb&#34;&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;&lt;span style=&#34;color:#bbb&#34;&gt;          &lt;/span&gt;&lt;span style=&#34;color:#309;font-weight:bold&#34;&gt;method&lt;/span&gt;:&lt;span style=&#34;color:#bbb&#34;&gt; &lt;/span&gt;get&lt;span style=&#34;color:#bbb&#34;&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;h2 id=&#34;deploying-an-existing-flask-app-to-aws-lambda&#34;&gt;Deploying an existing Flask app to AWS Lambda&lt;/h2&gt;
&lt;p&gt;The good news is that we can also leverage the Serverless framework to deploy Flask apps - and without needing much change to the existing project. This section assumes that you have an AWS account already that you can use. If not, then you can sign-up from &lt;a href=&#34;https://aws.amazon.com&#34; target=&#34;_blank&#34; rel=&#34;noopener noreferrer&#34; &gt;their website&lt;/a&gt;.&lt;/p&gt;
&lt;p&gt;First off, we need to install the Serverless framework itself. This can be achieved through NPM: &lt;code&gt;npm install -g serverless&lt;/code&gt;.&lt;/p&gt;
&lt;p&gt;Next, we need to configure credentials that will allow Serverless to interact with your AWS account. To do so, use the IAM manager on the AWS console to generate a set of keys (an access key and secret access key) and then use the following command to configure Serverless to use them:&lt;/p&gt;
&lt;div class=&#34;highlight&#34;&gt;&lt;pre tabindex=&#34;0&#34; style=&#34;background-color:#f0f3f3;-moz-tab-size:2;-o-tab-size:2;tab-size:2;&#34;&gt;&lt;code class=&#34;language-shell&#34; data-lang=&#34;shell&#34;&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;serverless config credentials --provider aws --key &lt;ACCESS_KEY&gt; --secret &lt;SECRET_ACCESS_KEY&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;While you should try and restrict access as much as possible, the fastest (yet riskiest) approach is to use an IAM user with Administrator Access permissions. If you want to configure more security I recommend reading the &lt;a href=&#34;https://www.serverless.com/blog/abcs-of-iam-permissions&#34; target=&#34;_blank&#34; rel=&#34;noopener noreferrer&#34; &gt;Serverless docs&lt;/a&gt;.&lt;/p&gt;
&lt;p&gt;Once the above groundwork has been completed, you can proceed to create a new &lt;code&gt;serverless.yml&lt;/code&gt; file in the root of your Flask project:&lt;/p&gt;
&lt;div class=&#34;highlight&#34;&gt;&lt;pre tabindex=&#34;0&#34; style=&#34;background-color:#f0f3f3;-moz-tab-size:2;-o-tab-size:2;tab-size:2;&#34;&gt;&lt;code class=&#34;language-yml&#34; data-lang=&#34;yml&#34;&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;&lt;span style=&#34;color:#309;font-weight:bold&#34;&gt;service&lt;/span&gt;:&lt;span style=&#34;color:#bbb&#34;&gt; &lt;/span&gt;my-flask-app&lt;span style=&#34;color:#bbb&#34;&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;&lt;span style=&#34;color:#bbb&#34;&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;&lt;span style=&#34;color:#bbb&#34;&gt;&lt;/span&gt;&lt;span style=&#34;color:#309;font-weight:bold&#34;&gt;provider&lt;/span&gt;:&lt;span style=&#34;color:#bbb&#34;&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;&lt;span style=&#34;color:#bbb&#34;&gt;  &lt;/span&gt;&lt;span style=&#34;color:#309;font-weight:bold&#34;&gt;name&lt;/span&gt;:&lt;span style=&#34;color:#bbb&#34;&gt; &lt;/span&gt;aws&lt;span style=&#34;color:#bbb&#34;&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;&lt;span style=&#34;color:#bbb&#34;&gt;  &lt;/span&gt;&lt;span style=&#34;color:#309;font-weight:bold&#34;&gt;runtime&lt;/span&gt;:&lt;span style=&#34;color:#bbb&#34;&gt; &lt;/span&gt;python3.8&lt;span style=&#34;color:#bbb&#34;&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;&lt;span style=&#34;color:#bbb&#34;&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;&lt;span style=&#34;color:#bbb&#34;&gt;&lt;/span&gt;&lt;span style=&#34;color:#309;font-weight:bold&#34;&gt;plugins&lt;/span&gt;:&lt;span style=&#34;color:#bbb&#34;&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;&lt;span style=&#34;color:#bbb&#34;&gt;  &lt;/span&gt;- serverless-wsgi&lt;span style=&#34;color:#bbb&#34;&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;&lt;span style=&#34;color:#bbb&#34;&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;&lt;span style=&#34;color:#bbb&#34;&gt;&lt;/span&gt;&lt;span style=&#34;color:#309;font-weight:bold&#34;&gt;functions&lt;/span&gt;:&lt;span style=&#34;color:#bbb&#34;&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;&lt;span style=&#34;color:#bbb&#34;&gt;  &lt;/span&gt;&lt;span style=&#34;color:#309;font-weight:bold&#34;&gt;api&lt;/span&gt;:&lt;span style=&#34;color:#bbb&#34;&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;&lt;span style=&#34;color:#bbb&#34;&gt;    &lt;/span&gt;&lt;span style=&#34;color:#309;font-weight:bold&#34;&gt;handler&lt;/span&gt;:&lt;span style=&#34;color:#bbb&#34;&gt; &lt;/span&gt;wsgi_handler.handler&lt;span style=&#34;color:#bbb&#34;&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;&lt;span style=&#34;color:#bbb&#34;&gt;    &lt;/span&gt;&lt;span style=&#34;color:#309;font-weight:bold&#34;&gt;events&lt;/span&gt;:&lt;span style=&#34;color:#bbb&#34;&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;&lt;span style=&#34;color:#bbb&#34;&gt;      &lt;/span&gt;- &lt;span style=&#34;color:#309;font-weight:bold&#34;&gt;http&lt;/span&gt;:&lt;span style=&#34;color:#bbb&#34;&gt; &lt;/span&gt;ANY /&lt;span style=&#34;color:#bbb&#34;&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;&lt;span style=&#34;color:#bbb&#34;&gt;      &lt;/span&gt;- &lt;span style=&#34;color:#309;font-weight:bold&#34;&gt;http&lt;/span&gt;:&lt;span style=&#34;color:#bbb&#34;&gt; &lt;/span&gt;ANY {proxy&#43;}&lt;span style=&#34;color:#bbb&#34;&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;&lt;span style=&#34;color:#bbb&#34;&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;&lt;span style=&#34;color:#bbb&#34;&gt;&lt;/span&gt;&lt;span style=&#34;color:#309;font-weight:bold&#34;&gt;custom&lt;/span&gt;:&lt;span style=&#34;color:#bbb&#34;&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;&lt;span style=&#34;color:#bbb&#34;&gt;  &lt;/span&gt;&lt;span style=&#34;color:#309;font-weight:bold&#34;&gt;wsgi&lt;/span&gt;:&lt;span style=&#34;color:#bbb&#34;&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;&lt;span style=&#34;color:#bbb&#34;&gt;    &lt;/span&gt;&lt;span style=&#34;color:#309;font-weight:bold&#34;&gt;app&lt;/span&gt;:&lt;span style=&#34;color:#bbb&#34;&gt; &lt;/span&gt;app.app&lt;span style=&#34;color:#bbb&#34;&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;Don’t worry too much about the &lt;code&gt;wsgi_handler.handler&lt;/code&gt; and &lt;code&gt;events&lt;/code&gt; parts - essentially these ensure that all HTTP requests to the service will get routed through to your app via a special handler that Serverless will setup for us.&lt;/p&gt;
&lt;p&gt;This setup assumes your root Flask file is named &lt;code&gt;app&lt;/code&gt; and that your Flask instance within this file is also named &lt;code&gt;app&lt;/code&gt; (in the &lt;code&gt;custom.wsgi&lt;/code&gt; attribute above), so you may need to change this if it doesn’t match your project setup.&lt;/p&gt;
&lt;p&gt;Another thing to note is the new &lt;code&gt;plugins&lt;/code&gt; block. Here we declare that our application requires the &lt;a href=&#34;https://www.serverless.com/plugins/serverless-wsgi&#34; target=&#34;_blank&#34; rel=&#34;noopener noreferrer&#34; &gt;&lt;code&gt;serverless-wsgi&lt;/code&gt;&lt;/a&gt; plugin, which will do much of the heavy lifting.&lt;/p&gt;
&lt;p&gt;To make use of the plugin, you’ll need to add it to your project as a dependency by running &lt;code&gt;serverless plugin install -n serverless-wsgi&lt;/code&gt;. As long as your Flask project dependencies are listed in a &lt;code&gt;requirements.txt&lt;/code&gt; file, you can now deploy your app by simply running &lt;code&gt;serverless deploy&lt;/code&gt;. After a few minutes, the framework will complete the deployment and will print out the URL to your new service.&lt;/p&gt;
&lt;h2 id=&#34;tweaking-the-deployment&#34;&gt;Tweaking the deployment&lt;/h2&gt;
&lt;p&gt;There are various ways to adjust the environment of your deployed service. For example, you can change the amount of memory assigned to your function, make use of environment variables (e.g. for database connection strings or mail server URLs), define roles for your functions to work with other AWS services, and much more.&lt;/p&gt;
&lt;p&gt;I recommend taking a look at the &lt;a href=&#34;https://www.serverless.com/framework/docs/providers/aws/guide/serverless.yml&#34; target=&#34;_blank&#34; rel=&#34;noopener noreferrer&#34; &gt;Serverless documentation&lt;/a&gt; to understand more about what options are available.&lt;/p&gt;
&lt;p&gt;If you want to use a custom domain for your service, then you can either set this up yourself in API Gateway through the AWS console or by using the &lt;a href=&#34;https://github.com/amplify-education/serverless-domain-manager&#34; target=&#34;_blank&#34; rel=&#34;noopener noreferrer&#34; &gt;&lt;code&gt;serverless-domain-manager&lt;/code&gt;&lt;/a&gt; plugin. Either way you will need to have your domain managed using &lt;a href=&#34;https://aws.amazon.com/route53&#34; target=&#34;_blank&#34; rel=&#34;noopener noreferrer&#34; &gt;Route 53&lt;/a&gt;.&lt;/p&gt;
&lt;h2 id=&#34;serverless-caveats&#34;&gt;Serverless caveats&lt;/h2&gt;
&lt;p&gt;Whilst the benefits offered by serverless delivery are strong, there are also some things to bear in mind - particularly when it comes to avoiding unexpected costs. Lambda functions bill per 100 milliseconds of execution time, and so long-running functions may be cut short (unless you tweak the duration allowance on the Lambda function, which can be up to 5 minutes long).&lt;/p&gt;
&lt;p&gt;Additionally, if your Flask app makes use of concurrency (e.g. if you use threads to background longer-running tasks, like email-sending), then this may not play nicely with Lambda, since the function may get terminated once a response is generated and returned.&lt;/p&gt;
&lt;p&gt;I outlined some extra things to watch out for &lt;a href=&#34;https://wilw.dev/blog/2021/01/03/scaling-serverless&#34;&gt;in a recent article&lt;/a&gt;, so take a look through that if you want to read more on these.&lt;/p&gt;
&lt;p&gt;Generally speaking, however, serverless apps are quite a cheap and risk-free way to experiment and get early prototypes off the ground. So, if you’re familiar with Flask (or other WSGI frameworks) and want an easy and scalable way to deploy your app, then perhaps this approach could be useful for your next project.&lt;/p&gt;
</content:encoded>
    </item>
    
    <item>
      <title>Migrating from Google Photos: Nextcloud, Piwigo, Mega, and pCloud</title>
      <link>https://wilw.dev/blog/2021/02/24/google-photos-pcloud/</link>
      <pubDate>Wed, 24 Feb 2021 13:46:00 +0000</pubDate>
      
      <guid>https://wilw.dev/blog/2021/02/24/google-photos-pcloud/</guid>
      
        <category>100daystooffload</category>
      
        <category>technology</category>
      
        <category>opinion</category>
      
      
        <enclosure url="https://wilw.dev/media/blog/header-google-photos-pcloud.png" type="image/png"/>
      
      <content:encoded>&lt;p&gt;By now I’m sure everyone has heard the horror stories about people (seemingly-) randomly losing access to their Google accounts. Often the account closures are reported to have been accompanied with vague automated notifications from Google complaining that the account-holder violated their terms in some way, but without any specific details or an offer of appeal or process to resolve the “issues” and reinstate the accounts.&lt;/p&gt;
&lt;p&gt;As such, these events usually mark the end of the road for the victims’ presence and data on Google platforms - including Gmail, Drive, Photos, YouTube - without having any option to extract the data out first. This could be years’ worth of documents, family photos, emails, Google Play purchases, and much more (ever used “Sign in with Google” on another service, for example?).&lt;/p&gt;
&lt;p&gt;Some affected people are fortunate to have a large social media following to ensure that their posts describing this treatment can traverse the networks enough to get through to someone close to Google who can try and elevate it in order to get accounts reinstated. However, for most people this is not possible.&lt;/p&gt;
&lt;p&gt;The &lt;a href=&#34;https://twitter.com/Demilogic&#34; target=&#34;_blank&#34; rel=&#34;noopener noreferrer&#34; &gt;creator of Stardew Valley&lt;/a&gt; recently &lt;a href=&#34;https://twitter.com/Demilogic/status/1358661840402845696&#34; target=&#34;_blank&#34; rel=&#34;noopener noreferrer&#34; &gt;found himself locked out of his 15-year-old Google account&lt;/a&gt; - even whilst involved in a key ongoing deal with Stadia, which he has since pulled out from due to feeling mal-treated. There are many similar stories available, and probably thousands more we never hear about.&lt;/p&gt;
&lt;p&gt;Of course, I am sure there are legitimate reasons for many accounts to be removed and that the original intentions behind these automated systems were good. Either way, this still just worries me. Whilst I haven’t (at least, I don’t think?) done anything to violate any terms, I just don’t want to take the risk and wake up one morning to find I have lost 15 years’ worth of emails, photos, and documents.&lt;/p&gt;
&lt;h2 id=&#34;what-are-the-alternatives&#34;&gt;What are the alternatives?&lt;/h2&gt;
&lt;p&gt;These days there are so many services that compete with Google’s own offerings. For example, &lt;a href=&#34;https://duckduckgo.com&#34; target=&#34;_blank&#34; rel=&#34;noopener noreferrer&#34; &gt;DuckDuckGo&lt;/a&gt; is excellent for web search - though several times a day I do need to revert to Google search for more complex queries (which I can do by prepending DuckDuckGo search queries with &lt;code&gt;g!&lt;/code&gt;). There are &lt;a href=&#34;https://restoreprivacy.com/google-alternatives&#34; target=&#34;_blank&#34; rel=&#34;noopener noreferrer&#34; &gt;many websites that list good alternatives to Google services&lt;/a&gt;, and I won’t bang on about these ideas here - it’s up to you what you prefer to use, of course, and this type of thing has been covered many times before.&lt;/p&gt;
&lt;p&gt;Personally, I’ve used my own domain to send and receive email (using &lt;a href=&#34;https://www.fastmail.com&#34; target=&#34;_blank&#34; rel=&#34;noopener noreferrer&#34; &gt;Fastmail&lt;/a&gt;) for several years now, and self-host my files and documents using &lt;a href=&#34;https://nextcloud.com&#34; target=&#34;_blank&#34; rel=&#34;noopener noreferrer&#34; &gt;Nextcloud&lt;/a&gt;. I don’t really use YouTube or have much data tied-up in the other Google offerings.&lt;/p&gt;
&lt;p&gt;However the one service I do rely on still is Google Photos. To be fair, this is a fantastic service - the apps seamlessly back everything up, the search is great (it’s Google’s bread-and-butter, after all), and I can easily and instantly find specific photos from two decades ago, or from any time in-between. It’s also super fast. I’d never found a good-enough replacement for media storage and so I never made the leap.&lt;/p&gt;
&lt;h2 id=&#34;the-problems-with-media-storage&#34;&gt;The problems with media storage&lt;/h2&gt;
&lt;p&gt;Images and videos - espcially with modern cameras and phones - take up a &lt;em&gt;huge&lt;/em&gt; amount of space. I take a few pictures every day, and on my messenger apps I sometimes like to save images I receive from friends and family too.&lt;/p&gt;
&lt;p&gt;This has resulted in a collection of over 84,000 pictures and videos in a mostly-continuous stream since 1998 - the year our family got our first digital camera. There are also digitised versions of photos from as early as 1959 on there too. Whilst this is not a massive collection by any standards these days, it forms a significant part of my own data footprint.&lt;/p&gt;
&lt;p&gt;Whilst I was happy with using Google for this one area, I would get so nervous every time I read one of those “deleted accounts” stories that it got to the point where last month I finally committed to making a change.&lt;/p&gt;
&lt;p&gt;In the meantime I needed to try and get my stuff out of Google Photos. The service lets you download 500 images at a time from the web interface, but that would have taken forever. The other option was to use &lt;a href=&#34;https://google.com/takeout&#34; target=&#34;_blank&#34; rel=&#34;noopener noreferrer&#34; &gt;Google Takeout&lt;/a&gt;. I did this and shortly after received an email containing links to download 48 different archives of data.&lt;/p&gt;
&lt;p&gt;&lt;img src=&#34;https://wilw.dev/media/blog/google-takeout-email.png&#34; alt=&#34;Email from Google Takout listing lots of download buttons&#34;&gt;&lt;/p&gt;
&lt;p&gt;When I downloaded a couple of examples, I saw that they seemed to contain a lot of JSON metadata files and not many actual photos. I imagined I’d have to download the whole lot to try and make sense of it all and manually piece bits together. I thought I’d leave that for now whilst I continued my search for an alternative service, and come back to that problem later.&lt;/p&gt;
&lt;h2 id=&#34;the-search-for-a-google-photos-alternative&#34;&gt;The search for a Google Photos alternative&lt;/h2&gt;
&lt;p&gt;The first job was to identify a new process/system for media storage. I had a few acceptance criteria in mind;&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;It needed to be affordable (not necessarily as cheap as &lt;a href=&#34;https://one.google.com&#34; target=&#34;_blank&#34; rel=&#34;noopener noreferrer&#34; &gt;Google One&lt;/a&gt;, but not bank-breaking either).&lt;/li&gt;
&lt;li&gt;I needed it to be quickly and easily navigable (i.e. to easily move to a particular date to find photos).&lt;/li&gt;
&lt;li&gt;It had to have some type of auto-sync from my phone’s photo gallery (I am too lazy to remember to manually “back-up” things - I need automation!).&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;I was already using Nextcloud for my documents anyway, and the Nextcloud app (which is brilliant) also includes an auto-upload feature for photos. However, I find Nextcloud gets a bit slow and grinds my server to a halt when viewing (I guess it processes things in the background?) large and recently-uploaded photos. Also, my VPS provider’s pricing (not unreasonable) would mean forking out the best part of $400 a year for the required block storage - which would only increase every year as my library gets bigger.&lt;/p&gt;
&lt;p&gt;I also considered &lt;a href=&#34;https://piwigo.org&#34; target=&#34;_blank&#34; rel=&#34;noopener noreferrer&#34; &gt;Piwigo&lt;/a&gt;, which looks great and is &lt;a href=&#34;https://piwigo.org/testimonials&#34; target=&#34;_blank&#34; rel=&#34;noopener noreferrer&#34; &gt;reported to be very fast&lt;/a&gt;. However the self-hosted option would have the same pricing implications as Nextcloud (above), and the hosted offering would be &lt;a href=&#34;https://piwigo.com/pricing&#34; target=&#34;_blank&#34; rel=&#34;noopener noreferrer&#34; &gt;significantly more&lt;/a&gt; if I was to include videos too. I think Piwigo is aimed more at photographers maintaining and sharing albums rather than for use as a personal photo storage solution.&lt;/p&gt;
&lt;p&gt;I &lt;a href=&#34;https://fosstodon.org/web/statuses/105692084325464954&#34; target=&#34;_blank&#34; rel=&#34;noopener noreferrer&#34; &gt;recently tooted&lt;/a&gt; out to the community about this problem and I got some great responses back. One idea in particular caught my eye: &lt;a href=&#34;https://mega.nz&#34; target=&#34;_blank&#34; rel=&#34;noopener noreferrer&#34; &gt;Mega&lt;/a&gt;. I had used Mega before a while back, and the apps and web interfaces seemed to have come on a long way in recent years. After a bit of research I decided to choose this option. It seemed secure (with client-side encryption), quick, and the apps had the features I needed.&lt;/p&gt;
&lt;p&gt;I went to pay for Mega (using the web browser), and it redirected me to a very dodgy-looking payment website - this threw me a little. I went back to the checkout page to see if I had clicked the wrong thing, clicked “confirm” again, and this time it took me to an entirely &lt;em&gt;different&lt;/em&gt; (but still sort of dodgy-looking) payment site. I’ve set-up &lt;a href=&#34;https://stripe.com&#34; target=&#34;_blank&#34; rel=&#34;noopener noreferrer&#34; &gt;Stripe&lt;/a&gt; a few times before, and know it’s pretty trivial these days to accept card payments on your own site, and so alarm bells began to ring a little. My paranoid security-focused self was put off enough to continue my search.&lt;/p&gt;
&lt;h2 id=&#34;migrating-to-pcloud&#34;&gt;Migrating to pCloud&lt;/h2&gt;
&lt;p&gt;That’s when I stumbled upon the Swiss-based &lt;a href=&#34;https://www.pcloud.com&#34; target=&#34;_blank&#34; rel=&#34;noopener noreferrer&#34; &gt;pCloud&lt;/a&gt; on a Reddit thread discussing storage alternatives. It seems to be pretty feature-matched with Mega, despite not offering client-side encryption out-of-the-box - but then neither does Google Photos. Additionally, pCloud offers both US and European servers.&lt;/p&gt;
&lt;p&gt;pCloud’s apps have similar functions to Mega, and the service also has the added bonus of offering a Google Drive integration! Hopefully this would mean I wouldn’t need to spend ages traversing that Google Takeout mess. The service also offers integrations with Dropbox, OneDrive, and some social networking platforms.&lt;/p&gt;
&lt;p&gt;I signed-up and paid - without needing to go to any dodgy sites. I then linked my Google account and waited for the magic to happen.&lt;/p&gt;
&lt;p&gt;It was a little slow. I know there was a fair amount of data, and I imagine the combination of this plus Google rate-limiting and other factors contributed to the speed too. I checked every few hours on the progress; there’s a sort of indicator (a folder count), but otherwise there was no way to really check what was going on. After a couple of days I noticed it had stopped (or “aborted”) by itself.&lt;/p&gt;
&lt;p&gt;&lt;img src=&#34;https://wilw.dev/media/blog/pcloud-google-drive.png&#34; alt=&#34;Screenshot of pCloud, showing Google Drive import aborted&#34;&gt;&lt;/p&gt;
&lt;p&gt;I had a quick browse through what pCloud had brought through and could see it had got to around July 2019 before it had had enough. This was OK - it had imported the vast majority and I was happy enough to run through the last couple of years’ worth of content on Google Photos, downloading 500 photos at a time to manually upload to pCloud in order to plug the gap.&lt;/p&gt;
&lt;p&gt;I then un-linked my Google account from pCloud. I turned off Google Photos auto-upload from my phone and instead all new media now gets auto-uploaded to pCloud. Job done.&lt;/p&gt;
&lt;h2 id=&#34;final-thoughts&#34;&gt;Final thoughts&lt;/h2&gt;
&lt;p&gt;pCloud’s navigation seems to be pretty quick, and uploading content is also very fast. It’s not &lt;em&gt;perfect&lt;/em&gt;, though (is anything?) - viewing photos on the app can take a few seconds to generate/retrieve thumbnails, and it doesn’t have the smoothness that Google Photos offers.&lt;/p&gt;
&lt;p&gt;However, it’s great for now. I have a &lt;em&gt;“tangible”&lt;/em&gt; folder of media that feels more portable in case I ever need to move again. pCloud also has clear channels for communication if I do ever need to get in touch, and I certainly feel as though I am less subject to automated judgments from unruly algorithms.&lt;/p&gt;
</content:encoded>
    </item>
    
    <item>
      <title>The Glamour of Cyberpunk and the Road to Solarpunk</title>
      <link>https://wilw.dev/blog/2021/02/20/solarpunk/</link>
      <pubDate>Sat, 20 Feb 2021 21:42:00 +0000</pubDate>
      
      <guid>https://wilw.dev/blog/2021/02/20/solarpunk/</guid>
      
        <category>100daystooffload</category>
      
        <category>opinion</category>
      
      
      <content:encoded>&lt;p&gt;A few months ago I stumbled across this article: &lt;a href=&#34;https://thedorkweb.substack.com/p/towards-a-solarpunk-future&#34; target=&#34;_blank&#34; rel=&#34;noopener noreferrer&#34; &gt;Beyond Cyberpunk: Towards a Solarpunk Future&lt;/a&gt;. It was posted on the excellent blog &lt;em&gt;Tales from the Dork Web&lt;/em&gt;, by Steve Lord, which I can certainly recommend &lt;a href=&#34;https://thedorkweb.substack.com/subscribe&#34; target=&#34;_blank&#34; rel=&#34;noopener noreferrer&#34; &gt;subscribing to&lt;/a&gt;.&lt;/p&gt;
&lt;p&gt;I had never heard of the term “Solarpunk” before, but I read up more about it and the more I researched the more intrigued I became. Essentially it is defined as - more or less - the &lt;em&gt;opposite&lt;/em&gt; to the &lt;a href=&#34;https://en.wikipedia.org/wiki/Cyberpunk&#34; target=&#34;_blank&#34; rel=&#34;noopener noreferrer&#34; &gt;Cyberpunk&lt;/a&gt; subculture, and I think we’re at a bit of a fork in the road from which either future could become a reality.&lt;/p&gt;
&lt;h2 id=&#34;cyberpunk&#34;&gt;Cyberpunk&lt;/h2&gt;
&lt;p&gt;Cyberpunk (&lt;em&gt;not&lt;/em&gt; the game by CD Projekt) is a term that describes a potential future setting that is pretty dystopian: there is a large “wealth gap” between the rich and poor; people live in dark and cramped accommodations, have mostly unhealthy existences, and are governed by a small number of large private corporations. The growth of these companies, however, allows citizens of the Cyberpunk future to be equipped with some pretty nice pieces of technology for communication, leisure &amp; media, travel, automation, and anything else.&lt;/p&gt;
&lt;p&gt;In a nutshell, it’s often described as “high-tech, low-life”.&lt;/p&gt;
&lt;p&gt;Whilst it sounds (to some?) like a gloomy outlook, I love the dark and lonely imagery, the artwork, stories and subculture that has emerged from other people who are also fascianted by this movement. You’ve probably seen such scenes yourself in pictures, movies, books, and games that adopt the Cyberpunk setting. The &lt;a href=&#34;https://www.reddit.com/r/ImaginaryCyberpunk&#34; target=&#34;_blank&#34; rel=&#34;noopener noreferrer&#34; &gt;r/ImaginaryCyberpunk subreddit&lt;/a&gt; community also often posts excellent and emotive content.&lt;/p&gt;
&lt;p&gt;I love this image: &lt;a href=&#34;https://www.artstation.com/artwork/R3QNee&#34; target=&#34;_blank&#34; rel=&#34;noopener noreferrer&#34; &gt;Oris City by Darko Mitev&lt;/a&gt; and I can certainly recommend checking out more of his work and tutorials too. I love all of the atmosphere and detail.&lt;/p&gt;
&lt;p&gt;Despite the “glamour”, interesting and exciting stories and movies, politics, and other cultural pieces that emerge from it, Cyberpunk describes a gloomy future that I imagine most people do not want to actually experience.&lt;/p&gt;
&lt;h2 id=&#34;solarpunk&#34;&gt;Solarpunk&lt;/h2&gt;
&lt;p&gt;I think we’re at a bit of a weird, but pivotal, point in time right now - from (geo-)political, societal and technological perspectives - in that the Cyberpunk dystopia is becoming a little unblurred. With ever-mounting consumerism, capitalism, bad choices regarding energy production, mass surveillance (from both private companies and governments), and much more, our reality certainly feels as though it is moving towards a point where some of the elements that comprise Cyberpunk do not feel too far-fetched at all.&lt;/p&gt;
&lt;p&gt;The present feels pivotal because whilst there are excellent efforts being made to reverse some of these positions around the world (from local recycling schemes and zero-waste manufacturers through to fights for human rights and rallies around liberal activists), these processes only become effective and impactive if they are considered and actioned by society &lt;em&gt;as a whole&lt;/em&gt;. While there are are still enough members that continue to wallow in seemingly-backward ideologies and refuse to become involved or make any of the needed adjustments, then change as a society cannot happen.&lt;/p&gt;
&lt;p&gt;However, on a more positive note, if such challenges can be solved - and the right choices made now and in the near future - then a whole new potential future opens its doors: one that might be described as &lt;em&gt;Solarpunk&lt;/em&gt;.&lt;/p&gt;
&lt;p&gt;In a &lt;a href=&#34;https://en.wikipedia.org/wiki/Solarpunk&#34; target=&#34;_blank&#34; rel=&#34;noopener noreferrer&#34; &gt;Solarpunk future&lt;/a&gt; humanity is much more in-tune with the world around it, maintaining a focus on sustainability (in terms of energy production, consumerism, ecology, and &lt;em&gt;education&lt;/em&gt;), locality (in terms of sourcing materials and food, manufacturing, and the “do it yourself” movement), and - perhaps most importantly - an &lt;em&gt;attitude&lt;/em&gt; that promotes sharing and positivity.&lt;/p&gt;
&lt;p&gt;To me it’s not “hippyish” or necessarily to do with the adoption of socialism or the outright rejection of capitlism and associated ideologies - it’s more concerned with sensible &lt;em&gt;balances&lt;/em&gt; across many facets of society and its politics. Competitiveness and drives to “do better” are parts of what make us human, and can very much live hand-in-hand with the other points and aesthetics we’re talking about here.&lt;/p&gt;
&lt;p&gt;Nor is it a rejection of technology. In fact, from a technological perspective, forward-thinking efforts surrounding the &lt;a href=&#34;https://en.wikipedia.org/wiki/Free_and_open-source_software&#34; target=&#34;_blank&#34; rel=&#34;noopener noreferrer&#34; &gt;free and open-source software&lt;/a&gt; movement and privacy-first companies are certainly components I see that can help contribute to (and become a focus within) a more sustainable and fair world. Technology can continue to innovate, develop, and improve in either setting.&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;Solarpunk isn’t about doing your bit to save the world from climate collapse. Solarpunk is about building the world you want your grandchildren to grow old in. - &lt;a href=&#34;https://thedorkweb.substack.com/p/towards-a-solarpunk-future&#34; target=&#34;_blank&#34; rel=&#34;noopener noreferrer&#34; &gt;Steve Lord&lt;/a&gt;&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;We’ve already seen some fantastic real-world efforts that can be considered part of this movement - from architecture and transport through to self-repair and home agriculture. I love the &lt;a href=&#34;https://containergardening.wordpress.com/2011/09/07/bottle-tower-gardening-how-to-start-willem-van-cotthem&#34; target=&#34;_blank&#34; rel=&#34;noopener noreferrer&#34; &gt;bottle farm&lt;/a&gt; idea included in the post I mentioned at the start of this article, and want to try this myself.&lt;/p&gt;
&lt;p&gt;There are also the more obvious reflections, such as to fully embrace solar energy (and other renewables) as a source of power - both at an individual and industrial scale - and efforts concerned with maintaining green spaces in developing and urban areas. I think that the more mainstream and ubiquitous we can make all of these actions the more realistic a Solarpunk world can become.&lt;/p&gt;
&lt;p&gt;–&lt;/p&gt;
&lt;p&gt;&lt;em&gt;Note: this article only scratches the surface of the Cyberpunk and Solarpunk subcultures. It is aimed to be more of a primer to introduce the concepts behind these ideas and to perhaps pique the interest of readers enough to continue their own research.&lt;/em&gt;&lt;/p&gt;
</content:encoded>
    </item>
    
    <item>
      <title>A Year Without Answering my Phone</title>
      <link>https://wilw.dev/blog/2021/02/18/no-phone-answering/</link>
      <pubDate>Thu, 18 Feb 2021 21:01:00 +0000</pubDate>
      
      <guid>https://wilw.dev/blog/2021/02/18/no-phone-answering/</guid>
      
        <category>100daystooffload</category>
      
        <category>life</category>
      
        <category>opinion</category>
      
      
      <content:encoded>&lt;p&gt;This month marks a year from when I decided to (&lt;em&gt;mostly&lt;/em&gt; - see below) stop answering my phone. This was not because I wanted to be antisocial (quite the opposite), but because it’s become the wrong form of communication for me.&lt;/p&gt;
&lt;h2 id=&#34;why-did-i-stop&#34;&gt;Why did I stop?&lt;/h2&gt;
&lt;p&gt;Like many people, I am inundated with sales-y and spammy phonecalls. I have had the same mobile phone number since 2001 (that’s 20 years this year), which I am sort of proud of and would prefer to keep. However, careless (or malicious) entities over the years (and more than likely mistakes also made by my younger self) have meant that my number and name are now in the databases of many different types of agents - from insurance/legal company sales teams through to dodgy Bitcoin spam companies.&lt;/p&gt;
&lt;p&gt;It got to the point that the signal/noise ratio (“real” phone calls vs. unwanted) probably dropped to around 5%. At first, spam calls were easier to spot (they’d call from random UK cities), but recently calls started to come in from numbers starting with “07” (which designates a mobile number in the UK) and also more and more from the &lt;a href=&#34;https://en.wikipedia.org/wiki/List_of_dialling_codes_in_the_United_Kingdom&#34; target=&#34;_blank&#34; rel=&#34;noopener noreferrer&#34; &gt;area code&lt;/a&gt; of the city where I live - probably in the hope of appearing more legitimate to me.&lt;/p&gt;
&lt;p&gt;I also find talking on the phone sort of &lt;em&gt;stressful&lt;/em&gt;. I’m sure I’m not alone in that the &lt;em&gt;Phone&lt;/em&gt; app is probably the least-used part of my smart&#34;phone&#34;. For some reason, to me it just doesn’t feel natural, and - with the exception of close friends and family (and even them sometimes) - I’d much rather “talk” to people via IM or live text chat.&lt;/p&gt;
&lt;p&gt;I’m naturally pretty introverted so I get on better with channels that enable me to think and formulate comms in my own time.&lt;/p&gt;
&lt;p&gt;Unexpected and unscheduled calls are also pretty &lt;em&gt;rude&lt;/em&gt;, I think. Stephen Fry sums up essentially what I feel about this &lt;a href=&#34;https://youtu.be/7xXSw07zrio?t=211&#34; target=&#34;_blank&#34; rel=&#34;noopener noreferrer&#34; &gt;in this short but great clip from QI&lt;/a&gt; - that phoning someone out of the blue is really the equivalent to going up to that person and yelling at them, “speak to me now, speak to me now, speak to me now”. Without caring that they might be busy, stressed, not in the right frame of mind, or any number of other states.&lt;/p&gt;
&lt;p&gt;This is incredibly invasive to do to someone you don’t even &lt;em&gt;know&lt;/em&gt;.&lt;/p&gt;
&lt;h2 id=&#34;what-was-the-result&#34;&gt;What was the result?&lt;/h2&gt;
&lt;p&gt;In the end I made a pact that I would no longer answer the phone unless it was a pre-arranged call or from a number that I recognised - and only then close friends and family.&lt;/p&gt;
&lt;p&gt;I feel far more empowered and in control of my own time when I hear/see my phone ring - and I just silence it and let it ring out. The decision has already been made to purposefully miss the call and so there is no need for any anxiety that might accompany such unexpected calls.&lt;/p&gt;
&lt;p&gt;I sometimes choose to avoid calls from numbers I &lt;em&gt;do&lt;/em&gt; recognise. These callers (usually businesses I deal with) just follow-up with an email anyway to which I can respond when I’m ready - usually within the hour. If there is an emergency they can leave a voicemail, which I will get notified about and then choose how best to respond. Friends and family either feel the same as me or know me well enough so that I don’t need to miss their calls.&lt;/p&gt;
&lt;p&gt;Either way, I haven’t (knowingly) missed any events, appointments, insurance renewals, or whatever. I am going to carry on as I have been and I can certainly recommend this approach to you too if you feel the same way as me about unwanted phonecalls.&lt;/p&gt;
</content:encoded>
    </item>
    
    <item>
      <title>The Midnight Library by Matt Haig</title>
      <link>https://wilw.dev/blog/2021/02/13/midnight-library/</link>
      <pubDate>Sat, 13 Feb 2021 21:17:00 +0000</pubDate>
      
      <guid>https://wilw.dev/blog/2021/02/13/midnight-library/</guid>
      
        <category>100daystooffload</category>
      
        <category>book</category>
      
      
      <content:encoded>&lt;p&gt;Last week I read &lt;a href=&#34;https://www.goodreads.com/book/show/52578297-the-midnight-library&#34; target=&#34;_blank&#34; rel=&#34;noopener noreferrer&#34; &gt;The Midnight Library&lt;/a&gt; by &lt;a href=&#34;https://www.goodreads.com/author/show/76360.Matt_Haig&#34; target=&#34;_blank&#34; rel=&#34;noopener noreferrer&#34; &gt;Matt Haig&lt;/a&gt;. The book won the 2020 &lt;a href=&#34;https://www.goodreads.com/award/show/21332-goodreads-choice-award&#34; target=&#34;_blank&#34; rel=&#34;noopener noreferrer&#34; &gt;Goodreads Choice Award for Fiction&lt;/a&gt;.&lt;/p&gt;
&lt;p&gt;&lt;img src=&#34;https://wilw.dev/media/blog/midnight-library.jpg&#34; alt=&#34;The Midnight Library cover&#34;&gt;&lt;/p&gt;
&lt;p&gt;“Set” in Bedford, England, the story starts by introducing the main character - Nora Seed - who feels completely down. She is depressed and thinks that she has nothing further to contribute to her own life or to the lives of the few people around her.&lt;/p&gt;
&lt;p&gt;On the day she decides she no longer wants to live, she is fired from her job, her cat dies, and other events occur which help cement her decision. However, as she dies she is transported to a place that exists between life and death: The Midnight Library.&lt;/p&gt;
&lt;p&gt;Here she is presented with the infinite number of books that make up the lives that could have been had she made different choices in the past - whether those were big or small (such as choosing whether to have a tea or coffee) or something more obviously impactful. Either way, they can contribute to a complete change in life direction.&lt;/p&gt;
&lt;p&gt;She has the option to begin living these different lives by considering the &lt;em&gt;regrets&lt;/em&gt; she has about the decisions she made in her root life. As she “visits” her other lives she reflects on the decisions that led her to that point, and also realises the power in the choices she makes in their ability to also drastically affect the lives of those around her.&lt;/p&gt;
&lt;p&gt;Whilst I feel that the book was perhaps not as deep as it could have been, it is my opinion that this was an intentional design by the author as it made the experience more of a canvas in order for the reader to make their own reflections.&lt;/p&gt;
&lt;p&gt;Some of the key thought takeaways for me were around the knowledge that whilst the decisions one person makes may benefit them, they may not be beneficial for everyone. Whilst of course it is important to consider the happiness and wellbeing of yourself as well as those affected by your decisions, one needs to live and experience the variety of life without feeling paranoid about making the decisions that you feel are the right ones.&lt;/p&gt;
&lt;p&gt;The book made me reflect on some of my own decisions. I know the grass isn’t always greener but that the choices are always there to be made if I want or need a change - it is never too late.&lt;/p&gt;
&lt;p&gt;The premise of the story sounds like it could be depressing, however I did not find that at all. In many ways, it was the complete opposite: having an understanding of the power in your choices helps you realise that even when things feel at their worst, you are not powerless. There is always something you can do to make a change and choices to be made to gear yourself towards where you need to be.&lt;/p&gt;
&lt;p&gt;We all have regrets in our own lives, and decisions we wish we did (or didn’t) make, but these should not be dwelled upon or worried about. Instead we can consider them as the useful tools they are to help us make different or better decisions as we look forward and continue into the future.&lt;/p&gt;
</content:encoded>
    </item>
    
    <item>
      <title>SSH Jumping and Bastion Hosts</title>
      <link>https://wilw.dev/blog/2021/02/10/ssh-jumping-bastion-hosts/</link>
      <pubDate>Wed, 10 Feb 2021 22:11:00 +0000</pubDate>
      
      <guid>https://wilw.dev/blog/2021/02/10/ssh-jumping-bastion-hosts/</guid>
      
        <category>100daystooffload</category>
      
        <category>technology</category>
      
        <category>security</category>
      
      
        <enclosure url="https://wilw.dev/media/blog/header-ssh-jumping-bastion-hosts.png" type="image/png"/>
      
      <content:encoded>&lt;p&gt;For many small or personal services running on a VPS in the cloud, administration is often done by connecting directly to the server via SSH. Such servers should be hardened with firewalls, employ an SSHd config that denies root and password-based login, run &lt;a href=&#34;https://www.fail2ban.org&#34; target=&#34;_blank&#34; rel=&#34;noopener noreferrer&#34; &gt;fail2ban&lt;/a&gt;, and other services and practices.&lt;/p&gt;
&lt;p&gt;Linode has some &lt;a href=&#34;https://www.linode.com/docs/guides/securing-your-server&#34; target=&#34;_blank&#34; rel=&#34;noopener noreferrer&#34; &gt;great getting-started guides&lt;/a&gt; on the essentials of securing your server.&lt;/p&gt;
&lt;h2 id=&#34;protecting-sensitive-servers&#34;&gt;Protecting sensitive servers&lt;/h2&gt;
&lt;p&gt;In more complex production scenarios heightened security can be achieved by isolating application (webapp, API, database, etc.) servers from external internet traffic. This is usually done by placing these “sensitive/protected” servers in a private &lt;a href=&#34;https://en.wikipedia.org/wiki/Subnetwork&#34; target=&#34;_blank&#34; rel=&#34;noopener noreferrer&#34; &gt;subnet&lt;/a&gt;, without direct internet-facing network interfaces. This means that the server is not reachable from the outside world.&lt;/p&gt;
&lt;p&gt;In this type of scenario, outbound traffic from the sensitive server can be routed through a &lt;a href=&#34;https://en.wikipedia.org/wiki/Network_address_translation&#34; target=&#34;_blank&#34; rel=&#34;noopener noreferrer&#34; &gt;NAT gateway&lt;/a&gt; and inbound traffic can be funnelled through a &lt;a href=&#34;https://en.wikipedia.org/wiki/Load_balancing_%28computing%29&#34; target=&#34;_blank&#34; rel=&#34;noopener noreferrer&#34; &gt;load-balancer&lt;/a&gt; or reverse proxy server. In both these cases the NAT gateway and load-balancer would exist in public subnets (with internet-facing network interfaces) and can reach the sensitive server through private network interfaces in order to forward requests (e.g. web traffic).&lt;/p&gt;
&lt;p&gt;&lt;img src=&#34;https://wilw.dev/media/blog/ssh1.png&#34; alt=&#34;Diagram of public and private subnets, with a NAT gateway and load balancer&#34;&gt;&lt;/p&gt;
&lt;p&gt;Now the question is around how one &lt;em&gt;does&lt;/em&gt; manage the services running on the protected server, since it is no longer available to connect to. Traditionally this is done by introducing &lt;em&gt;bastion hosts&lt;/em&gt; into your network.&lt;/p&gt;
&lt;h2 id=&#34;bastion-hosts&#34;&gt;Bastion hosts&lt;/h2&gt;
&lt;p&gt;&lt;a href=&#34;https://en.wikipedia.org/wiki/Bastion_host&#34; target=&#34;_blank&#34; rel=&#34;noopener noreferrer&#34; &gt;Bastion hosts&lt;/a&gt; - like the NAT gateway and load balancers - sit in the public subnet and so they are available to the outside world. They often accept SSH connections, from which one can “jump” through to the protected servers through the bastion’s private networking interface.&lt;/p&gt;
&lt;p&gt;&lt;img src=&#34;https://wilw.dev/media/blog/ssh2.png&#34; alt=&#34;Adding a bastion host to the cloud infrastructure&#34;&gt;&lt;/p&gt;
&lt;p&gt;Bastion hosts should be hardened as much as possible (with firewalls and other network rules), and should run a limited set of services - in many cases simply SSHd.&lt;/p&gt;
&lt;p&gt;This server then enables administrators to connect through to the protected servers in order to carry out maintenance, upgrades, or other tasks.&lt;/p&gt;
&lt;h2 id=&#34;connecting-through-a-bastion-host&#34;&gt;Connecting through a bastion host&lt;/h2&gt;
&lt;p&gt;SSH port-forwarding is a widely-used concept, in which a secure tunnel to a service running on the protected server is opened via a port on the local machine (using &lt;code&gt;ssh&lt;/code&gt;’s &lt;code&gt;-L&lt;/code&gt; option).&lt;/p&gt;
&lt;p&gt;Another option is to use proxy &lt;em&gt;jumping&lt;/em&gt; (with the &lt;code&gt;-J&lt;/code&gt; option):&lt;/p&gt;
&lt;div class=&#34;highlight&#34;&gt;&lt;pre tabindex=&#34;0&#34; style=&#34;background-color:#f0f3f3;-moz-tab-size:2;-o-tab-size:2;tab-size:2;&#34;&gt;&lt;code class=&#34;language-shell&#34; data-lang=&#34;shell&#34;&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;ssh -J bastion.company.com protected.company-internal.com
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;In this example the user connects via the bastion through to the protected server at &lt;code&gt;protected.company-internal.com&lt;/code&gt;. Since you should be using key-based authentication to connect, you may also need to specify the private key path (with the &lt;code&gt;-i&lt;/code&gt; option), and also tell SSH to forward your agent to the bastion (using &lt;code&gt;-A&lt;/code&gt;) so it can continue the connection.&lt;/p&gt;
&lt;p&gt;This can make things hard to remember each time. You could write the command in a script, however it’s probably easier to use SSH’s own local configuration. To do so, you can add the following to your local &lt;code&gt;~/.ssh/config&lt;/code&gt; file:&lt;/p&gt;
&lt;pre tabindex=&#34;0&#34;&gt;&lt;code&gt;Host *.company-internal.com
  ProxyJump bastion.company.com
  User username
  IdentityFile /home/username/.ssh/identity
&lt;/code&gt;&lt;/pre&gt;&lt;p&gt;With that in place you can now simply run the following when you want to connect to the protected server:&lt;/p&gt;
&lt;div class=&#34;highlight&#34;&gt;&lt;pre tabindex=&#34;0&#34; style=&#34;background-color:#f0f3f3;-moz-tab-size:2;-o-tab-size:2;tab-size:2;&#34;&gt;&lt;code class=&#34;language-shell&#34; data-lang=&#34;shell&#34;&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;ssh protected.company-internal.com
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;&lt;em&gt;Note: depending on your system you may need to add the key to your local agent first, but you just need to do this once per login (&lt;code&gt;ssh-add ~/.ssh/identity&lt;/code&gt;)&lt;/em&gt;.&lt;/p&gt;
&lt;h2 id=&#34;a-note-on-dns&#34;&gt;A note on DNS&lt;/h2&gt;
&lt;p&gt;Generally I would probably avoid assigning public domain names to a bastion host, as this may invite unwanted attention and traffic (even if the host is secured). Instead you can just include the IP address directly in the &lt;code&gt;ProxyJump&lt;/code&gt; line of &lt;code&gt;.ssh/config&lt;/code&gt;. &lt;em&gt;I used domain names in the examples above to make the process clearer&lt;/em&gt;.&lt;/p&gt;
&lt;p&gt;Also, in the above example I refer to the &lt;code&gt;company-internal.com&lt;/code&gt; domain for use within the private network. This domain should only be resolvable within members of the private network - either by using an internal DNS server or by simply modifying &lt;code&gt;/etc/hosts&lt;/code&gt; on the bastion. Alternatively you can just use the private IP address for the protected server on the &lt;code&gt;Host&lt;/code&gt; line of &lt;code&gt;.ssh/config&lt;/code&gt;.&lt;/p&gt;
&lt;h2 id=&#34;additional-notes&#34;&gt;Additional notes&lt;/h2&gt;
&lt;p&gt;In setups like this you may also want to consider the following:&lt;/p&gt;
&lt;h3 id=&#34;private-keys&#34;&gt;Private keys&lt;/h3&gt;
&lt;p&gt;Don’t provision these on your bastion host. Instead use agent forwarding (as described above). You’ll need to add your public keys to both the bastion and protected servers.&lt;/p&gt;
&lt;h3 id=&#34;restrict-source-network&#34;&gt;Restrict source network&lt;/h3&gt;
&lt;p&gt;For extra security you can restrict SSH connections to your bastion only from trusted networks (e.g. your office network or a VPN).&lt;/p&gt;
&lt;p&gt;Similarly, restrict protected servers such that they only accept SSH traffic from the bastion, and not from other servers on the network.&lt;/p&gt;
&lt;h3 id=&#34;make-use-of-managed-services-ifwhen-possible&#34;&gt;Make use of managed services if/when possible&lt;/h3&gt;
&lt;p&gt;For extra security you can use managed services when they are available. For example, if you use AWS then you can make use of a combination of VPCs, subnets, NAT gateways, elastic load balancing, security groups, Route 53, and other services to secure your hosts and control your network. You can of course set this up on your own servers without relying on managed services.&lt;/p&gt;
&lt;p&gt;Either way, I hope this post has helped shed light on some simple ways to improve network security for your applications and services.&lt;/p&gt;
</content:encoded>
    </item>
    
    <item>
      <title>Using Monica to Help Manage your Personal Relationships</title>
      <link>https://wilw.dev/blog/2021/02/07/monica-personal-crm/</link>
      <pubDate>Sun, 07 Feb 2021 19:31:00 +0000</pubDate>
      
      <guid>https://wilw.dev/blog/2021/02/07/monica-personal-crm/</guid>
      
        <category>100daystooffload</category>
      
        <category>technology</category>
      
        <category>life</category>
      
        <category>selfhost</category>
      
      
      <content:encoded>&lt;p&gt;Many people no longer feel comfortable using Facebook. Whether you were never a member to begin with or you’ve had an account but chosen to remove yourself from the service, or you’ve simply tried to start using it less - either way, it’s no surprise given the way that they, across their family of products (including Instagram and WhatsApp), operate in terms of your own data and time.&lt;/p&gt;
&lt;p&gt;This is a huge subject on its own and it’s really up for everyone to make their own minds up when it comes to their own stance. It’s been widely discussed pretty much everywhere, and there are &lt;a href=&#34;https://www.quitfacebook.org&#34; target=&#34;_blank&#34; rel=&#34;noopener noreferrer&#34; &gt;loads of resources available on this handy website&lt;/a&gt; if you’re interested in understanding more about what goes on behind the scenes on these platforms.&lt;/p&gt;
&lt;h2 id=&#34;staying-in-the-loop&#34;&gt;Staying in the loop&lt;/h2&gt;
&lt;p&gt;Anyway this isn’t another post about Facebook, but one of the things that &lt;em&gt;is&lt;/em&gt; useful about that particular platform is its birthday reminder system in which you automatically receive an email from Facebook if it happens to be one of your friend’s birthdays that day. In itself, this is of course simply a mechanism to try and get you to re-engage with the platform - such as to send your friend a direct message on Messenger or to post something on their timeline.&lt;/p&gt;
&lt;p&gt;However, it is nice to get messages on your birthday, and nice to imagine that someone you only speak to a couple of times a year has the headspace to &lt;em&gt;remember&lt;/em&gt; that today is your special day. Even though you both know that it’s because Facebook has sent a reminder with an easy CTA.&lt;/p&gt;
&lt;p&gt;The good news is that there are still lots of services that help you remember key events without needing to rely on Facebook. Of course you can set-up calendars (many mail providers have in-built calendar facilities that can sync to your client with CalDAV), but you may want to remember other things too - such as anniversaries, friends’ pets’ names, that time you helped your cousin move house, and more. Quickly all of this info ends up distributed between a number of systems and becomes hard to look-up and manage (unless you’re super organised).&lt;/p&gt;
&lt;h2 id=&#34;monica-the-personal-relationship-manager&#34;&gt;Monica: the “Personal Relationship Manager”&lt;/h2&gt;
&lt;p&gt;What we need is a personal &lt;em&gt;CRM&lt;/em&gt; (“customer relationship manager”), which can do all of this for us. And thankfully such systems exist - such as &lt;a href=&#34;https://www.monicahq.com&#34; target=&#34;_blank&#34; rel=&#34;noopener noreferrer&#34; &gt;Monica&lt;/a&gt;.&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;Monica is the single best investment you can make to have better relationships. - &lt;a href=&#34;https://www.monicahq.com/pricing&#34; target=&#34;_blank&#34; rel=&#34;noopener noreferrer&#34; &gt;monicahq.com&lt;/a&gt;.&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;Monica is a piece of &lt;a href=&#34;https://github.com/monicahq/monica&#34; target=&#34;_blank&#34; rel=&#34;noopener noreferrer&#34; &gt;open-source software&lt;/a&gt; that can handle all of this for you as a “Personal Relationship Manager” (in their words) - and much more. You can sign-up on &lt;a href=&#34;https://app.monicahq.com/register&#34; target=&#34;_blank&#34; rel=&#34;noopener noreferrer&#34; &gt;their website&lt;/a&gt; and pay a small ongoing subscription fee to cover the server costs. Alternatively, you can easily self-host it on your own server.&lt;/p&gt;
&lt;p&gt;&lt;img src=&#34;https://wilw.dev/media/blog/monica.png&#34; alt=&#34;The Monica dashboard homepage&#34; title=&#34;This is what my Monica homepage looks like&#34;&gt;&lt;/p&gt;
&lt;p&gt;I’ve been using it (the self-hosted option) for some time now, and &lt;a href=&#34;https://github.com/monicahq/monica#features&#34; target=&#34;_blank&#34; rel=&#34;noopener noreferrer&#34; &gt;love its features&lt;/a&gt;. I get automatic email notifications in-time to remind me about key events, I can keep track of the birthdays of my friends’ kids, remember gifts I have been given, friend life events, jobs, and more.&lt;/p&gt;
&lt;p&gt;Although I still want to spend some further time setting it up and adding more details about the people I know, it already helps me to include richer information when I message friends and family and to remember the things I really should be anyway.&lt;/p&gt;
&lt;p&gt;Monica looks great, works fine on my phone web browser as well as my desktop browser, and also has an API that allows you to build your own workflows or to connect it to other services.&lt;/p&gt;
&lt;p&gt;If you find yourself forgetting birthdays and important information about friends and family, or if you just want to log relationships more effectively, then I certainly recommend giving it a go.&lt;/p&gt;
&lt;h2 id=&#34;how-to-self-host-monica&#34;&gt;How to self-host Monica&lt;/h2&gt;
&lt;p&gt;I host Monica on a relatively small VPS. It’s lightweight and it happily runs alongside a few other services.&lt;/p&gt;
&lt;p&gt;I usually prefer using Docker to host things like this as it helps keep things isolated when running multiple services on the same machine. I have an Nginx container (with several virtual hosts) that proxies requests through to the appropriate services.&lt;/p&gt;
&lt;p&gt;The Monica Team kindly maintain an official &lt;a href=&#34;https://hub.docker.com/_/monica&#34; target=&#34;_blank&#34; rel=&#34;noopener noreferrer&#34; &gt;Docker image&lt;/a&gt;. I went for the Apache version (as I already have Nginx in-place for TLS, etc.) for which there is an example &lt;a href=&#34;https://docs.docker.com/compose&#34; target=&#34;_blank&#34; rel=&#34;noopener noreferrer&#34; &gt;Docker Compose&lt;/a&gt; config available on the official Monica image page. The documentation also explains how to get your first user setup.&lt;/p&gt;
&lt;p&gt;One of the main advantages of Monica is its ability to keep you updated without you needing to login and check-up on things. It does this by sending you emails, and for this to work you’ll need to add a bit of extra configuration to your Docker Compose file, as &lt;a href=&#34;https://github.com/monicahq/monica/blob/master/docs/installation/mail.md&#34; target=&#34;_blank&#34; rel=&#34;noopener noreferrer&#34; &gt;described on this page&lt;/a&gt;. Just add the extra variables to your &lt;code&gt;environment&lt;/code&gt; section in &lt;code&gt;docker-compose.yml&lt;/code&gt;. The article mentions Amazon SES, however you can use your own mail provider’s SMTP/IMAP server settings here (e.g. &lt;a href=&#34;https://www.mailgun.com&#34; target=&#34;_blank&#34; rel=&#34;noopener noreferrer&#34; &gt;Mailgun&lt;/a&gt;).&lt;/p&gt;
&lt;p&gt;If you plan to use &lt;a href=&#34;https://www.linode.com&#34; target=&#34;_blank&#34; rel=&#34;noopener noreferrer&#34; &gt;Linode&lt;/a&gt; to host your Monica service (which is a great choice), you may just need to open up a quick support ticket with them so that they can make sure your account is allowed to send traffic on standard email ports (e.g. 25 and 587), which they sometimes restrict on new accounts to help fight spam.&lt;/p&gt;
&lt;h2 id=&#34;contribute&#34;&gt;Contribute&lt;/h2&gt;
&lt;p&gt;If you want to contribute to this great open-source project, then there are &lt;a href=&#34;https://github.com/monicahq/monica#contribute&#34; target=&#34;_blank&#34; rel=&#34;noopener noreferrer&#34; &gt;guides available on GitHub&lt;/a&gt;.&lt;/p&gt;
</content:encoded>
    </item>
    
    <item>
      <title>Add icing to your websites using pattern.css</title>
      <link>https://wilw.dev/blog/2021/02/06/pattern-css/</link>
      <pubDate>Sat, 06 Feb 2021 19:34:00 +0000</pubDate>
      
      <guid>https://wilw.dev/blog/2021/02/06/pattern-css/</guid>
      
        <category>100daystooffload</category>
      
        <category>technology</category>
      
        <category>css</category>
      
      
      <content:encoded>&lt;p&gt;Shapes and patterns can be leveraged in user interfaces to guide your users, draw attention to content, lend weight or emphasis, or just for aesthetics and decoration.&lt;/p&gt;
&lt;p&gt;Layout and styling on the web is typically handled using CSS, however mastering CSS to the level where you can confidently take advantage of more advanced features is definitely not easy. I’ve been developing for the web almost full-time for a decade and I’m still pretty crap when it comes to doing complex stuff with CSS.&lt;/p&gt;
&lt;p&gt;That said, some people have done some &lt;a href=&#34;https://a.singlediv.com&#34; target=&#34;_blank&#34; rel=&#34;noopener noreferrer&#34; &gt;mindblowing things using CSS and just a single &lt;code&gt;div&lt;/code&gt; element&lt;/a&gt;.&lt;/p&gt;
&lt;p&gt;The &lt;a href=&#34;https://www.toptal.com/designers/subtlepatterns&#34; target=&#34;_blank&#34; rel=&#34;noopener noreferrer&#34; &gt;Subtle Patterns&lt;/a&gt; website has been around for years - it’s a great resource for discovering and accessing nice textures and backgrounds for your creations. There are also some nice libraries in CSS that let you describe patterns programmatically, and this comes with the added performance advantages that CSS provides (as browsers are pretty performant when it comes to CSS).&lt;/p&gt;
&lt;p&gt;&lt;a href=&#34;https://bansal.io/pattern-css&#34; target=&#34;_blank&#34; rel=&#34;noopener noreferrer&#34; &gt;pattern.css&lt;/a&gt; (created by &lt;a href=&#34;https://github.com/bansal-io&#34; target=&#34;_blank&#34; rel=&#34;noopener noreferrer&#34; &gt;bansal-io&lt;/a&gt;) is a great little CSS-only library for adding simple, but effective, patterns to your websites. For example, backgrounds for elements, false “block” shadows, or even within the text itself. All that’s needed are a few extra classes on your elements and the small (less than 1Kb when gzipped) library will do the rest.&lt;/p&gt;
&lt;p&gt;To get started, you can add the library to your project using your normal JavaScript package manager (e.g. &lt;code&gt;yarn add pattern.css&lt;/code&gt;). Then either include the CSS file in your HTML or, if you’re using React or another framework/builder that allows you to import CSS directly, you can:&lt;/p&gt;
&lt;div class=&#34;highlight&#34;&gt;&lt;pre tabindex=&#34;0&#34; style=&#34;background-color:#f0f3f3;-moz-tab-size:2;-o-tab-size:2;tab-size:2;&#34;&gt;&lt;code class=&#34;language-jsx&#34; data-lang=&#34;jsx&#34;&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;&lt;span style=&#34;color:#069;font-weight:bold&#34;&gt;import&lt;/span&gt; &lt;span style=&#34;color:#c30&#34;&gt;&#39;pattern.css/dist/pattern.css&#39;&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;Once that’s done it’s just a matter of &lt;a href=&#34;https://bansal.io/pattern-css#usage&#34; target=&#34;_blank&#34; rel=&#34;noopener noreferrer&#34; &gt;adding classes&lt;/a&gt; to your markup. All the &lt;code&gt;pattern.css&lt;/code&gt; classes start with &lt;code&gt;pattern&lt;/code&gt;, followed by the type of pattern (e.g. &lt;code&gt;-diagonal-stripes&lt;/code&gt;), followed by the “size” of the pattern (e.g. &lt;code&gt;-sm&lt;/code&gt;).&lt;/p&gt;
&lt;p&gt;For example, to build a &lt;code&gt;div&lt;/code&gt; with a chunky zig-zag patterned background you just need to use:&lt;/p&gt;
&lt;div class=&#34;highlight&#34;&gt;&lt;pre tabindex=&#34;0&#34; style=&#34;background-color:#f0f3f3;-moz-tab-size:2;-o-tab-size:2;tab-size:2;&#34;&gt;&lt;code class=&#34;language-html&#34; data-lang=&#34;html&#34;&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;&lt;&lt;span style=&#34;color:#309;font-weight:bold&#34;&gt;div&lt;/span&gt; &lt;span style=&#34;color:#309&#34;&gt;class&lt;/span&gt;&lt;span style=&#34;color:#555&#34;&gt;=&lt;/span&gt;&lt;span style=&#34;color:#c30&#34;&gt;&#34;pattern-zigzag-lg&#34;&lt;/span&gt;&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;  ...
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;&lt;/&lt;span style=&#34;color:#309;font-weight:bold&#34;&gt;div&lt;/span&gt;&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;To change the colour of the pattern just set a &lt;code&gt;color&lt;/code&gt; style on the element. If the element also has a &lt;code&gt;backgroundColor&lt;/code&gt; then this will display through the transparent bits:&lt;/p&gt;
&lt;div class=&#34;highlight&#34;&gt;&lt;pre tabindex=&#34;0&#34; style=&#34;background-color:#f0f3f3;-moz-tab-size:2;-o-tab-size:2;tab-size:2;&#34;&gt;&lt;code class=&#34;language-html&#34; data-lang=&#34;html&#34;&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;&lt;&lt;span style=&#34;color:#309;font-weight:bold&#34;&gt;div&lt;/span&gt; &lt;span style=&#34;color:#309&#34;&gt;class&lt;/span&gt;&lt;span style=&#34;color:#555&#34;&gt;=&lt;/span&gt;&lt;span style=&#34;color:#c30&#34;&gt;&#34;pattern-diagonal-stripes-md&#34;&lt;/span&gt; &lt;span style=&#34;color:#309&#34;&gt;style&lt;/span&gt;&lt;span style=&#34;color:#555&#34;&gt;=&lt;/span&gt;&lt;span style=&#34;color:#c30&#34;&gt;&#34;color: red; backgroundColor: yellow&#34;&lt;/span&gt;&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;  ...
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;&lt;/&lt;span style=&#34;color:#309;font-weight:bold&#34;&gt;div&lt;/span&gt;&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;Have a read through &lt;a href=&#34;https://bansal.io/pattern-css#hero&#34; target=&#34;_blank&#34; rel=&#34;noopener noreferrer&#34; &gt;the documentation&lt;/a&gt; for examples and further pattern types. It’s quick to get the hang of and far more effective to use if - like me - you find some of the complexities of CSS hard to get your head around!&lt;/p&gt;
</content:encoded>
    </item>
    
    <item>
      <title>React State Management with Zustand</title>
      <link>https://wilw.dev/blog/2021/02/05/react-state-zustand/</link>
      <pubDate>Fri, 05 Feb 2021 23:46:00 +0000</pubDate>
      
      <guid>https://wilw.dev/blog/2021/02/05/react-state-zustand/</guid>
      
        <category>100daystooffload</category>
      
        <category>technology</category>
      
        <category>javascript</category>
      
        <category>react</category>
      
      
      <content:encoded>&lt;h2 id=&#34;react-state&#34;&gt;React state&lt;/h2&gt;
&lt;p&gt;React state management is what gives the library its reactiveness. It’s what makes it so easy to build performant data-driven applications that dynamically update based on the underlying data. In this example the app would automatically update the calculation result as the user types in the input boxes:&lt;/p&gt;
&lt;div class=&#34;highlight&#34;&gt;&lt;pre tabindex=&#34;0&#34; style=&#34;background-color:#f0f3f3;-moz-tab-size:2;-o-tab-size:2;tab-size:2;&#34;&gt;&lt;code class=&#34;language-jsx&#34; data-lang=&#34;jsx&#34;&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;&lt;span style=&#34;color:#069;font-weight:bold&#34;&gt;import&lt;/span&gt; React, { useState } from &lt;span style=&#34;color:#c30&#34;&gt;&#39;react&#39;&lt;/span&gt;;
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;&lt;span style=&#34;color:#069;font-weight:bold&#34;&gt;function&lt;/span&gt; MultiplicationCalculator() {
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;  &lt;span style=&#34;color:#069;font-weight:bold&#34;&gt;const&lt;/span&gt; [number1, setNumber1] &lt;span style=&#34;color:#555&#34;&gt;=&lt;/span&gt; useState(&lt;span style=&#34;color:#f60&#34;&gt;0&lt;/span&gt;);
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;  &lt;span style=&#34;color:#069;font-weight:bold&#34;&gt;const&lt;/span&gt; [number2, setNumber2] &lt;span style=&#34;color:#555&#34;&gt;=&lt;/span&gt; useState(&lt;span style=&#34;color:#f60&#34;&gt;0&lt;/span&gt;);
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;  &lt;span style=&#34;color:#069;font-weight:bold&#34;&gt;return&lt;/span&gt; ( &lt;&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;    &lt;&lt;span style=&#34;color:#309;font-weight:bold&#34;&gt;input&lt;/span&gt; &lt;span style=&#34;color:#309&#34;&gt;value&lt;/span&gt;&lt;span style=&#34;color:#555&#34;&gt;=&lt;/span&gt;{number1} &lt;span style=&#34;color:#309&#34;&gt;onChange&lt;/span&gt;&lt;span style=&#34;color:#555&#34;&gt;=&lt;/span&gt;{e =&gt; setNumber1(&lt;span style=&#34;color:#366&#34;&gt;parseInt&lt;/span&gt;(e.target.value))} /&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;    &lt;&lt;span style=&#34;color:#309;font-weight:bold&#34;&gt;input&lt;/span&gt; &lt;span style=&#34;color:#309&#34;&gt;value&lt;/span&gt;&lt;span style=&#34;color:#555&#34;&gt;=&lt;/span&gt;{number2} &lt;span style=&#34;color:#309&#34;&gt;onChange&lt;/span&gt;&lt;span style=&#34;color:#555&#34;&gt;=&lt;/span&gt;{e =&gt; setNumber2(&lt;span style=&#34;color:#366&#34;&gt;parseInt&lt;/span&gt;(e.target.value))} /&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;    &lt;&lt;span style=&#34;color:#309;font-weight:bold&#34;&gt;p&lt;/span&gt;&gt;The result is {number1 &lt;span style=&#34;color:#555&#34;&gt;*&lt;/span&gt; number2}.&lt;/&lt;span style=&#34;color:#309;font-weight:bold&#34;&gt;p&lt;/span&gt;&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;  &lt;/&gt; );
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;}
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;&lt;img src=&#34;https://wilw.dev/media/blog/zustand1.png&#34; alt=&#34;The resultant React app, showing two text inputs and a result line&#34;&gt;&lt;/p&gt;
&lt;p&gt;The entire function will re-run on each state change (the &lt;code&gt;setNumber1&lt;/code&gt; and &lt;code&gt;setNumber2&lt;/code&gt; functions) in order to reactively update the result text. The multiplication itself could be calculated in a &lt;code&gt;useEffect&lt;/code&gt; but it is simpler to look at it as shown.&lt;/p&gt;
&lt;p&gt;This is totally fine for many apps, however this quickly becomes unmanageable when you need to share state (e.g. &lt;code&gt;number1&lt;/code&gt;) between this component and another component - and ensure that a state change in the former can be reflected in the latter - whether it’s an ancestor, descendant, or a more distant component. Of course, you can pass the state variables (and the associated &lt;code&gt;setState&lt;/code&gt; functions) from a parent down as &lt;code&gt;props&lt;/code&gt; to child components, but as soon as you’re doing this more than a handful of times or in cases where state needs to be shared across distant components this quickly becomes hard to maintain or understand.&lt;/p&gt;
&lt;p&gt;An example of shared state might be to store the details about the currently logged-in user in an app. A navigation bar component would need to know about the user state to show a link to the correct profile page, and another component may need access to the same state in order to allow the user to change their name.&lt;/p&gt;
&lt;h2 id=&#34;context-and-redux&#34;&gt;Context and Redux&lt;/h2&gt;
&lt;p&gt;This is by no means a new problem. Many of these issues are solved using React’s &lt;a href=&#34;https://reactjs.org/docs/context.html&#34; target=&#34;_blank&#34; rel=&#34;noopener noreferrer&#34; &gt;Context API&lt;/a&gt; and there are also libraries like Redux that are useful in perhaps more complex scenarios - it’s much more opinionated and involves a fair bit of extra code that may be overkill in many apps. Adding just a small piece of state (e.g. a new text input), and the ability to alter it, to Redux involves updating reducers, creating an action, dispatchers, and wiring things through to your components using &lt;code&gt;connect&lt;/code&gt;, &lt;code&gt;mapStateToProps&lt;/code&gt;, and &lt;code&gt;mapDispatchToProps&lt;/code&gt;. Plus you’ll need the relevant provider higher up.&lt;/p&gt;
&lt;p&gt;Redux is certainly a fantastic library, however, and I use it in many apps. &lt;a href=&#34;https://changelog.com/posts/when-and-when-not-to-reach-for-redux&#34; target=&#34;_blank&#34; rel=&#34;noopener noreferrer&#34; &gt;This post&lt;/a&gt; is useful and discusses the cases in which you may (or may not) want to use Redux.&lt;/p&gt;
&lt;h2 id=&#34;zustand&#34;&gt;Zustand&lt;/h2&gt;
&lt;p&gt;In this post I want to talk about another option that is perhaps quicker and easier to use, expecially for those newer to React (though it’s also great for more seasoned React developers) - &lt;a href=&#34;https://github.com/pmndrs/zustand&#34; target=&#34;_blank&#34; rel=&#34;noopener noreferrer&#34; &gt;zustand&lt;/a&gt;. Not only is this the German word for “state”, it’s also a nice and succinct library for state management for React.&lt;/p&gt;
&lt;p&gt;The zustand library is pretty concise, so you shouldn’t need to add too much extra code. To get started just add it as a dependency to your project (e.g. &lt;code&gt;yarn add zustand&lt;/code&gt;). Now let’s rewrite the earlier multiplication example but using zustand.&lt;/p&gt;
&lt;p&gt;First, define a &lt;em&gt;store&lt;/em&gt; for your app. This will contain all of the values you want to keep in your global state, as well as the functions that allow those values to change (&lt;em&gt;mutators&lt;/em&gt;). In our store, we’ll extract out the state for &lt;code&gt;number1&lt;/code&gt; and &lt;code&gt;number2&lt;/code&gt; we used in our component from earlier, and the appropriate update functions (e.g. &lt;code&gt;setNumber1&lt;/code&gt;), into the store:&lt;/p&gt;
&lt;div class=&#34;highlight&#34;&gt;&lt;pre tabindex=&#34;0&#34; style=&#34;background-color:#f0f3f3;-moz-tab-size:2;-o-tab-size:2;tab-size:2;&#34;&gt;&lt;code class=&#34;language-jsx&#34; data-lang=&#34;jsx&#34;&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;&lt;span style=&#34;color:#069;font-weight:bold&#34;&gt;import&lt;/span&gt; React from &lt;span style=&#34;color:#c30&#34;&gt;&#39;react&#39;&lt;/span&gt;;
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;&lt;span style=&#34;color:#069;font-weight:bold&#34;&gt;import&lt;/span&gt; create from &lt;span style=&#34;color:#c30&#34;&gt;&#39;zustand&#39;&lt;/span&gt;;
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;&lt;span style=&#34;color:#069;font-weight:bold&#34;&gt;const&lt;/span&gt; useStore &lt;span style=&#34;color:#555&#34;&gt;=&lt;/span&gt; create((set) =&gt; ({
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;  number1&lt;span style=&#34;color:#555&#34;&gt;:&lt;/span&gt; &lt;span style=&#34;color:#f60&#34;&gt;0&lt;/span&gt;,
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;  number2&lt;span style=&#34;color:#555&#34;&gt;:&lt;/span&gt; &lt;span style=&#34;color:#f60&#34;&gt;0&lt;/span&gt;,
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;  setNumber1&lt;span style=&#34;color:#555&#34;&gt;:&lt;/span&gt; (x) =&gt; set(() =&gt; ({ number1&lt;span style=&#34;color:#555&#34;&gt;:&lt;/span&gt; x })),
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;  setNumber2&lt;span style=&#34;color:#555&#34;&gt;:&lt;/span&gt; (x) =&gt; set(() =&gt; ({ number2&lt;span style=&#34;color:#555&#34;&gt;:&lt;/span&gt; x })),
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;}));
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;Now - in the same file - we can go ahead and rewrite our component such that it now uses this store instead of its own local state:&lt;/p&gt;
&lt;div class=&#34;highlight&#34;&gt;&lt;pre tabindex=&#34;0&#34; style=&#34;background-color:#f0f3f3;-moz-tab-size:2;-o-tab-size:2;tab-size:2;&#34;&gt;&lt;code class=&#34;language-jsx&#34; data-lang=&#34;jsx&#34;&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;&lt;span style=&#34;color:#069;font-weight:bold&#34;&gt;function&lt;/span&gt; MultiplicationCalculator() {
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;  &lt;span style=&#34;color:#069;font-weight:bold&#34;&gt;const&lt;/span&gt; { number1, number2, setNumber1, setNumber2 } &lt;span style=&#34;color:#555&#34;&gt;=&lt;/span&gt; useStore();
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;  &lt;span style=&#34;color:#069;font-weight:bold&#34;&gt;return&lt;/span&gt; ( &lt;&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;    &lt;&lt;span style=&#34;color:#309;font-weight:bold&#34;&gt;input&lt;/span&gt; &lt;span style=&#34;color:#309&#34;&gt;value&lt;/span&gt;&lt;span style=&#34;color:#555&#34;&gt;=&lt;/span&gt;{number1} &lt;span style=&#34;color:#309&#34;&gt;onChange&lt;/span&gt;&lt;span style=&#34;color:#555&#34;&gt;=&lt;/span&gt;{e =&gt; setNumber1(&lt;span style=&#34;color:#366&#34;&gt;parseInt&lt;/span&gt;(e.target.value))} /&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;    &lt;&lt;span style=&#34;color:#309;font-weight:bold&#34;&gt;input&lt;/span&gt; &lt;span style=&#34;color:#309&#34;&gt;value&lt;/span&gt;&lt;span style=&#34;color:#555&#34;&gt;=&lt;/span&gt;{number2} &lt;span style=&#34;color:#309&#34;&gt;onChange&lt;/span&gt;&lt;span style=&#34;color:#555&#34;&gt;=&lt;/span&gt;{e =&gt; setNumber2(&lt;span style=&#34;color:#366&#34;&gt;parseInt&lt;/span&gt;(e.target.value))} /&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;    &lt;&lt;span style=&#34;color:#309;font-weight:bold&#34;&gt;p&lt;/span&gt;&gt;The result is {number1 &lt;span style=&#34;color:#555&#34;&gt;*&lt;/span&gt; number2}.&lt;/&lt;span style=&#34;color:#309;font-weight:bold&#34;&gt;p&lt;/span&gt;&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;  &lt;/&gt; );
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;}
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;That’s it - we now have a React app that uses zustand. As before, the component function runs each time the store’s state changes, and zustand ensures things are kept up-to-date.&lt;/p&gt;
&lt;p&gt;In the example above the two blocks of code are in the same file. However, the power of zustand becomes particularly useful when the store is shared amongst several components across different parts of your app to provide “global state”.&lt;/p&gt;
&lt;p&gt;For example, the &lt;code&gt;useStore&lt;/code&gt; variable could be declared and exported from a file named &lt;code&gt;store.js&lt;/code&gt; somewhere in your app’s file structure. Then, when a component needs to access its variables or mutator functions it just needs to - for example, &lt;code&gt;import useStore from &#39;path/to/store&#39;&lt;/code&gt; - and then use &lt;a href=&#34;https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Operators/Destructuring_assignment&#34; target=&#34;_blank&#34; rel=&#34;noopener noreferrer&#34; &gt;object destructuring&lt;/a&gt; (as on line 11 above) to pull out the needed variables and functions.&lt;/p&gt;
&lt;p&gt;It’s worth checking out &lt;a href=&#34;https://github.com/pmndrs/zustand&#34; target=&#34;_blank&#34; rel=&#34;noopener noreferrer&#34; &gt;the documentation&lt;/a&gt; since zustand is super flexible and can be used in ways that help improve performance, such as taking advantage of memoizing and state slicing. It also makes what can be tricky in other such libraries - e.g. asynchronous state updates - trivial.&lt;/p&gt;
&lt;p&gt;If you’ve already got an established app using another state management system it may not be worth migrating everything over. But give zustand a go in your next project if you’re looking for straight forward, yet powerful, state management.&lt;/p&gt;
</content:encoded>
    </item>
    
    <item>
      <title>RSS: The Rise and Fall... and Rise Again</title>
      <link>https://wilw.dev/blog/2021/02/03/rss-rise-fall-rise/</link>
      <pubDate>Wed, 03 Feb 2021 22:16:00 +0000</pubDate>
      
      <guid>https://wilw.dev/blog/2021/02/03/rss-rise-fall-rise/</guid>
      
        <category>100daystooffload</category>
      
        <category>technology</category>
      
        <category>opinion</category>
      
      
      <content:encoded>&lt;p&gt;Many people would consider RSS - Really Simple Syndication - to be a relic of the past. However I think it has been making a comeback.&lt;/p&gt;
&lt;p&gt;RSS is a mechanism by which people can automatically receive updates from individual websites, similar to how you might follow another user on a social networking service. Software known as RSS &lt;em&gt;readers&lt;/em&gt; can be used to subscribe to RSS &lt;em&gt;feeds&lt;/em&gt; in order to receive these updates. As new content (e.g. a blog post) is published to an RSS-enabled website, its feed is updated and your RSS reader will show the new post the next time it refreshes. Many RSS readers have an interface similar to an email client, with read/unread states, folders, favourites, and more.&lt;/p&gt;
&lt;h2 id=&#34;the-rise&#34;&gt;The rise&lt;/h2&gt;
&lt;p&gt;RSS was &lt;a href=&#34;https://en.wikipedia.org/wiki/RSS&#34; target=&#34;_blank&#34; rel=&#34;noopener noreferrer&#34; &gt;first released&lt;/a&gt; in early 1999, and it steadily gained popularity amongst content producers and consumers, with adopters from media outlets and software implementations making their way into early Internet Eplorer and Firefox versions, amongst others. These were the days before the “real” &lt;a href=&#34;https://en.wikipedia.org/wiki/Web_2.0&#34; target=&#34;_blank&#34; rel=&#34;noopener noreferrer&#34; &gt;Web 2.0&lt;/a&gt; hit, and in which websites were very much more silos of information. Tools like RSS were powerful then because they enabled the easy &lt;em&gt;aggregation&lt;/em&gt; of information from multiple sources.&lt;/p&gt;
&lt;p&gt;Not too long after this (Web 2.0 ‘began’ in the mid 2000’s), and during the years ever since, mainstream social networks became ubiquitous. Many people flock(ed) to these as a way to share and subscribe (by following others) to receive updates in real time, several times a day and from lots of different people and organisations. These services enabled features far beyond aggregation by allowing easy sharing, rating (e.g. likes) and commentating such that today such services have become the primary means of information and news sharing and reception for many people.&lt;/p&gt;
&lt;h2 id=&#34;the-fall&#34;&gt;The fall(?)&lt;/h2&gt;
&lt;p&gt;At that time RSS was still very much “a thing” for many people (though the &lt;a href=&#34;https://en.wikipedia.org/wiki/Google_Reader#Discontinuation&#34; target=&#34;_blank&#34; rel=&#34;noopener noreferrer&#34; &gt;discontinuation of the hugely popular Google Reader in 2013&lt;/a&gt; was a bit of a bummer to these communities). However new people now joining the web scene would be far more likely to instead engage with these extremely well-funded, well-marketed, and &lt;em&gt;centralised&lt;/em&gt; social platforms - &lt;a href=&#34;https://www.thesocialdilemma.com&#34; target=&#34;_blank&#34; rel=&#34;noopener noreferrer&#34; &gt;perfectly engeineered to be addictive&lt;/a&gt;, entirely driven and propagated by &lt;a href=&#34;https://en.wikipedia.org/wiki/Fear_of_missing_out&#34; target=&#34;_blank&#34; rel=&#34;noopener noreferrer&#34; &gt;FOMO&lt;/a&gt;, and focused on content-sharing (even if the content is often &lt;a href=&#34;https://www.theguardian.com/technology/2021/jan/30/facebook-letting-fake-news-spreaders-profit-investigators-claim&#34; target=&#34;_blank&#34; rel=&#34;noopener noreferrer&#34; &gt;misinformation&lt;/a&gt;) - where &lt;em&gt;you&lt;/em&gt; are the product, rather than spend the time researching and subscribing to individual RSS feeds.&lt;/p&gt;
&lt;p&gt;To some commentators in this space the concept behind all of these social platforms is known as the &lt;em&gt;&lt;a href=&#34;https://jackcheng.com/essays/the-slow-web/#the-fast-web&#34; target=&#34;_blank&#34; rel=&#34;noopener noreferrer&#34; &gt;fast web&lt;/a&gt;&lt;/em&gt; - a web that tells you when and what information to consume rather than letting you make that decision for yourself. Facebook, Twitter, Instagram, and others all started as just a &lt;em&gt;chronological&lt;/em&gt; timeline of interesting content from friends and family. On all of these services today the “algorithm” determines what (and who) goes in your timeline, and it constantly learns what to feed you - and when - in order to get those few extra minutes from you each day. This is literally its business model.&lt;/p&gt;
&lt;p&gt;What used to be an innocent bookmarking tool, Twitter’s “favouriting” mechanism is now essentially a game of &lt;strong&gt;retweet roulette&lt;/strong&gt; in which the algorithm will every now and again choose to include your “bookmarks” (not just retweets) on the feeds of people who follow you. If that’s not anxiety-inducing or user-hostile then I don’t know what is!&lt;/p&gt;
&lt;p&gt;Of course this is something Facebook has done for a while too, except perhaps in a more sinister way - such as implying &lt;a href=&#34;https://www.baekdal.com/thoughts/facebook-graph-search-privacy-woes&#34; target=&#34;_blank&#34; rel=&#34;noopener noreferrer&#34; &gt;a user has liked something when they haven’t at all&lt;/a&gt;.&lt;/p&gt;
&lt;p&gt;Other social networking tools can be more user-friendly. For example, the open-source &lt;a href=&#34;https://en.wikipedia.org/wiki/Mastodon_%28software%29&#34; target=&#34;_blank&#34; rel=&#34;noopener noreferrer&#34; &gt;Mastodon&lt;/a&gt; software powers distributed social networks that aren’t fuelled by addiction and instead give you more control over what you receive and where your posts go. However these tools still have some way to go to becoming anywere near mainstream.&lt;/p&gt;
&lt;p&gt;I want to caveat some of the above: I obviously don’t think any of this is the fault of the individual. These social platforms are fantastically easy places to set-up a web presence. Creating an Instagram, Facebook, or Tik-Tok account for you (or your business) takes literal seconds. Within a minute you can have your profile setup and be following a dozen people, and already getting engagement and “reactions” from others (remember those “Your friend, X, is now on Instagram!” type notifications?).&lt;/p&gt;
&lt;p&gt;With all of this power and efficiency at the fingertips it’s no wonder that people don’t create their own personal websites anymore, or feel the need to reach out to actively keep-up with other such sites. What’s the point in re-inventing the wheel when I can easily create a Facebook page for myself that includes an inbuilt blog “feed”, a space for links, photos, and more? And it’s “free”! The barrier to creating a self-owned personal space on the internet for yourself is considered too high for most people, and is probably still seen as “geeky” even if it does come with all the benefits of privacy and control.&lt;/p&gt;
&lt;p&gt;And I’m not saying that self-owned spaces, RSS, and that whole ecosystem are related, or the opposite to, mainstream social media; more that it is a useful way to compare and contrast different ways of accessing and disseminating information and the level of control one has over this.&lt;/p&gt;
&lt;p&gt;This probably feels like I’m going way off-piste, and I sort of have, but my key meaning here is that for several years the concept of RSS has evaporated from popular knowledge because people haven’t &lt;em&gt;needed&lt;/em&gt; it - either as a tool for receiving &lt;em&gt;or&lt;/em&gt; disseminating information. Ask your non-tech friends and family if they’ve heard of RSS (and know what it is for a bonus point) - I bet the positive response rate will be low in most cases, especially in younger respondents.&lt;/p&gt;
&lt;p&gt;Also, I don’t think this is solely the fault of the social giants. Online media outlets - which would have needed to rely on RSS for years before online social media became more mainstream - now often completely ignore it or treat it as a second-class citizen.&lt;/p&gt;
&lt;p&gt;The &lt;a href=&#34;https://www.bbc.co.uk/news&#34; target=&#34;_blank&#34; rel=&#34;noopener noreferrer&#34; &gt;BBC News website&lt;/a&gt; happily displays large friendly icons for Facebook, Twitter, and the like, but no mention of RSS (try &lt;code&gt;ctrl-F&lt;/code&gt;). In fact, you’ll probably need to search the web for “bbc rss” in order to find the RSS feeds that are &lt;a href=&#34;https://www.bbc.co.uk/news/10628494&#34; target=&#34;_blank&#34; rel=&#34;noopener noreferrer&#34; &gt;listed on a page that hasn’t been updated for over a decade&lt;/a&gt; and which still lists IE7 and the long-discontinued Google Reader as sensible options (though ironically I suppose this does indicate the stability and robustness of the RSS system).&lt;/p&gt;
&lt;h2 id=&#34;the-rise-again&#34;&gt;The rise again&lt;/h2&gt;
&lt;p&gt;Anyway, all that sounds a bit doom and gloom but I definitely think we are starting to see a shift in people’s attitude towards and - importantly, trust in - these big tech companies. Facebook’s recent attitiude towards information collection (and subsequent sharing) has &lt;a href=&#34;https://www.independent.co.uk/life-style/gadgets-and-tech/facebook-update-apple-privacy-ads-b1795916.html&#34; target=&#34;_blank&#34; rel=&#34;noopener noreferrer&#34; &gt;hit mainstream headlines&lt;/a&gt; and everyone must have seen &lt;a href=&#34;https://www.techradar.com/news/whatsapps-new-privacy-policy-requires-you-to-share-data-with-facebook&#34; target=&#34;_blank&#34; rel=&#34;noopener noreferrer&#34; &gt;Whatsapp’s popup about data sharing&lt;/a&gt;. Too much uncertainty undermines the trust in these platforms, and people have understandably sought out other options. A few weeks ago Telegram reported an addition of &lt;a href=&#34;https://www.androidpolice.com/2021/01/12/telegram-adds-25-million-new-users-in-just-72-hours-as-rival-apps-falter&#34; target=&#34;_blank&#34; rel=&#34;noopener noreferrer&#34; &gt;25 million new users within 72 hours&lt;/a&gt; as a result of these policy “changes”.&lt;/p&gt;
&lt;p&gt;My parents aren’t really tech-aware at all but even they were telling me last week on a video call about this “new app Signal” they had downloaded and begun to use with their friends - without any of my input.&lt;/p&gt;
&lt;p&gt;I’m not sure what it is, but people seem to &lt;em&gt;care&lt;/em&gt; more about their data these days. Whether that’s because of GDPR, the fact that coronavirus means people aren’t endlessly scrolling through social feeds on their daily commutes anymore, or something else or a mixture of everything. And that extends to being more picky about the information they receive too.&lt;/p&gt;
&lt;p&gt;Either way, I’ve noticed more and more &lt;a href=&#34;https://atthis.link/blog/2021/rss.html&#34; target=&#34;_blank&#34; rel=&#34;noopener noreferrer&#34; &gt;posts like this&lt;/a&gt; (and the subsequent &lt;a href=&#34;https://news.ycombinator.com/item?id=26014344&#34; target=&#34;_blank&#34; rel=&#34;noopener noreferrer&#34; &gt;reactions and discussions&lt;/a&gt;) recently, and the &lt;a href=&#34;https://100daystooffload.com&#34; target=&#34;_blank&#34; rel=&#34;noopener noreferrer&#34; &gt;#100DaysToOffload&lt;/a&gt; movement has brought about a surge in people - myself included, really - creating their own longer-form content, for which RSS is a perfect distribution mechanism.&lt;/p&gt;
&lt;p&gt;I think we’re on a bit of a brink representing a general - but real - change in attitude from people towards data, and the time that they choose to give to now lesser-trusted platforms. It is our responsibility to help educate about the alternative options so that those around us can make their own decisions. Whilst I am relatively new to RSS in the grand scheme of things (having only really started properly engaging with it about a year ago), it already makes me feel more in control of what I view, and when.&lt;/p&gt;
&lt;p&gt;Whilst this concept doesn’t need to be limited to RSS, it’s a great starting point as it’s easy to understand. It “feels” friendly, it helps power connections to the decentralised and &lt;a href=&#34;https://ar.al/2020/08/07/what-is-the-small-web/&#34; target=&#34;_blank&#34; rel=&#34;noopener noreferrer&#34; &gt;small web&lt;/a&gt;.&lt;/p&gt;
&lt;p&gt;It, as a concept, has no business model. Though of course you can pay for the software you use, and websites can make money through ads, but at least you have a &lt;em&gt;choice&lt;/em&gt; regarding who you subscribe to and the software you use to do it through (and &lt;a href=&#34;https://en.wikipedia.org/wiki/Comparison_of_feed_aggregators&#34; target=&#34;_blank&#34; rel=&#34;noopener noreferrer&#34; &gt;there are lots of choices&lt;/a&gt;). You aren’t tied into anything and it respects your privacy - you don’t need to “sign-up”, provide your details, and sites don’t know that &lt;em&gt;you&lt;/em&gt; personally have subscribed.&lt;/p&gt;
&lt;p&gt;RSS may be age-old, but it is an excellent way to still get the information you need as you begin to use mainstream social media less, and - although it doesn’t need to be slow in itself - it is a fantastic tool to combine with the growing and user-respecting world of the &lt;a href=&#34;https://jackcheng.com/essays/the-slow-web#timely-vs-real-time&#34; target=&#34;_blank&#34; rel=&#34;noopener noreferrer&#34; &gt;slow web&lt;/a&gt;, in which timeliness (where you’re in control) is far more important than “real-time”.&lt;/p&gt;
&lt;p&gt;–&lt;/p&gt;
&lt;h3 id=&#34;edit&#34;&gt;Edit&lt;/h3&gt;
&lt;p&gt;I’ve received some replies to this post that talk about the lack of mentions of the &lt;a href=&#34;https://en.wikipedia.org/wiki/Atom_%28Web_standard%29&#34; target=&#34;_blank&#34; rel=&#34;noopener noreferrer&#34; &gt;Atom standard&lt;/a&gt; and podcasts. RSS certainly is (and has been) a fantastic way to subscribe to podcasts. Its flexibility and ease of use has been a great tool for both content creators and consumers, and has helped to build the ecosystem of podcast apps and services we see today. And of course, there are other very useful distribution mechanisms and standards available for distributing information, such as Atom. This post was focused more on contrasting this family of systems with what many people may consider “mainstream” services, and how the wide adoption of the latter has perhaps had an effect on the former.&lt;/p&gt;
</content:encoded>
    </item>
    
    <item>
      <title>Blogging for Devs</title>
      <link>https://wilw.dev/blog/2021/02/02/blogging-for-devs/</link>
      <pubDate>Tue, 02 Feb 2021 20:31:00 +0000</pubDate>
      
      <guid>https://wilw.dev/blog/2021/02/02/blogging-for-devs/</guid>
      
        <category>100daystooffload</category>
      
        <category>life</category>
      
      
      <content:encoded>&lt;p&gt;A few months ago I discovered &lt;a href=&#34;https://bloggingfordevs.com&#34; target=&#34;_blank&#34; rel=&#34;noopener noreferrer&#34; &gt;Blogging for Devs&lt;/a&gt; - I think through &lt;a href=&#34;https://www.producthunt.com/posts/blogging-for-devs&#34; target=&#34;_blank&#34; rel=&#34;noopener noreferrer&#34; &gt;Product Hunt&lt;/a&gt; when it made it to #1 Product of the Day back in August last year.&lt;/p&gt;
&lt;p&gt;At the time blogging was something I had been thinking about quite a lot. I actively followed several other blogs - both from people I know and from others in the tech community - and it was clear that, in addition to producing content that was interesting to read by others, writing was something these bloggers actually enjoyed and found valuable too for their own learning and engagement with the community.&lt;/p&gt;
&lt;p&gt;I have also always enjoyed writing (you have to if you’re ever involved in research!). I was still posting things occasionally, and had been doing so for several years, but blogging had just never really got round to forming any part of my normal routine. It was certainly something I wanted to do more of - to write, engage more with likeminded people, and for all the other personal and professional benefits associated with consistent and frequent writing - and so this was clearly a habit I needed to learn to form.&lt;/p&gt;
&lt;p&gt;&lt;img src=&#34;https://wilw.dev/media/blog/bloggingfordevs.png&#34; alt=&#34;Blogging for Devs website&#34;&gt;&lt;/p&gt;
&lt;p&gt;Blogging for Devs is a course and newsletter created by &lt;a href=&#34;https://monicalent.com&#34; target=&#34;_blank&#34; rel=&#34;noopener noreferrer&#34; &gt;Monica Lent&lt;/a&gt; and, with all of this running through my head, I signed-up almost straight away.&lt;/p&gt;
&lt;p&gt;I don’t want to give away too much about Monica’s course or content (it’s free to &lt;a href=&#34;https://bloggingfordevs.com&#34; target=&#34;_blank&#34; rel=&#34;noopener noreferrer&#34; &gt;sign up yourself!&lt;/a&gt;), but one thing I found really valuable was actually something that happened right at the start of the course. After I signed-up I received an automated email asking &lt;em&gt;why&lt;/em&gt; I had chosen to sign-up and what I wanted to learn. Of course, I know this is largely to help Monica shape her course and to get an understanding of people’s needs, but I actually found it a super-helpful self-reflection.&lt;/p&gt;
&lt;p&gt;Why &lt;em&gt;didn’t&lt;/em&gt; I blog more? What was blocking me, even though it was something I actively wanted to do? After a while of thinking it boiled down to one main thing, which was my &lt;strong&gt;confidence&lt;/strong&gt; and, in particular a fear of what people would think if they read it (especially if they knew me!) and also a worry of writing about things no-one is actually interested in (“why would anyone want to read this?”). I summarised this and wrote it back to Monica’s email, and she got back to me with a nice personal reply not long after.&lt;/p&gt;
&lt;p&gt;The course covers lots of topics - from SEO and branding through to actual blog content. However, my issue was still very much the whole confidence thing. One thing that became clear to me during the course is that the most important step in getting over that barrier, and then forming a habit - whether it’s getting up early, doing more exercise, or writing blog posts - is just to &lt;strong&gt;start doing it&lt;/strong&gt;.&lt;/p&gt;
&lt;p&gt;And I don’t mean tomorrow or next week, I mean &lt;strong&gt;today&lt;/strong&gt;. Just pick something to write about. If you’re just getting started it can be a quick post introducing yourslf (&lt;a href=&#34;https://writefreely.org&#34; target=&#34;_blank&#34; rel=&#34;noopener noreferrer&#34; &gt;WriteFreely&lt;/a&gt; is a great platform if you need one). If you’ve already got something going and want to write more (like me) then write a short post about something you’ve learned today - tech or not. The important thing is just to start doing it.&lt;/p&gt;
&lt;p&gt;Of course, not everything you post will be enjoyed by everyone, but that’s OK. It’s not always solely about your audience; you’re doing it for yourself too, remember.&lt;/p&gt;
&lt;p&gt;And also remember to sign-up to &lt;a href=&#34;https://bloggingfordevs.com&#34; target=&#34;_blank&#34; rel=&#34;noopener noreferrer&#34; &gt;Blogging for Devs&lt;/a&gt; today too. It’s a fantastic course. If you look back at my &lt;a href=&#34;https://wilw.dev/blog&#34;&gt;writing history&lt;/a&gt; you’ll notice the difference it’s had on me. I’ve blogged much more consistently and effectively since taking the course, and I’m still working through some of the content even today.&lt;/p&gt;
&lt;p&gt;Even if you’re already a seasoned blogger I’m sure you’ll pick up some extra tips and helpful insights, and the Blogging for Devs website also has a great &lt;a href=&#34;https://bloggingfordevs.com/trends&#34; target=&#34;_blank&#34; rel=&#34;noopener noreferrer&#34; &gt;Trends section&lt;/a&gt; to help you discover new blogs to follow.&lt;/p&gt;
</content:encoded>
    </item>
    
    <item>
      <title>Why not SQLite?</title>
      <link>https://wilw.dev/blog/2021/02/01/why-not-sqlite/</link>
      <pubDate>Mon, 01 Feb 2021 21:06:00 +0000</pubDate>
      
      <guid>https://wilw.dev/blog/2021/02/01/why-not-sqlite/</guid>
      
        <category>100daystooffload</category>
      
        <category>technology</category>
      
        <category>opinion</category>
      
      
      <content:encoded>&lt;p&gt;If you need a database for your next project, why not first consider if &lt;a href=&#34;https://sqlite.org&#34; target=&#34;_blank&#34; rel=&#34;noopener noreferrer&#34; &gt;SQLite&lt;/a&gt; might be a good option? And I don’t mean just for getting an MVP off the ground or for small personal systems; I mean for “real” production workloads.&lt;/p&gt;
&lt;p&gt;&lt;img src=&#34;https://wilw.dev/media/blog/sqlite.jpg&#34; alt=&#34;Why not Sqlite?&#34;&gt;&lt;/p&gt;
&lt;p&gt;Many people will be quick to jump on this with chimes of “it’s not designed for production”, but I think it depends on what is actually &lt;em&gt;meant&lt;/em&gt; by “production”? Sure, it’s not the right choice for every scenario - it wouldn’t work well in distributed workloads or for services expected to receive a very high volume of traffic - but it has been used successfully in many real-world cases.&lt;/p&gt;
&lt;p&gt;What made me feel the need to write this article was seeing this sentence in the &lt;a href=&#34;https://hub.docker.com/r/matrixdotorg/synapse/&#34; target=&#34;_blank&#34; rel=&#34;noopener noreferrer&#34; &gt;README of the Synapse Docker repo&lt;/a&gt;:&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;By default it uses a sqlite database; for production use you should connect it to a separate postgres database. - &lt;a href=&#34;https://hub.docker.com/r/matrixdotorg/synapse/&#34; target=&#34;_blank&#34; rel=&#34;noopener noreferrer&#34; &gt;matrixdotorg/synapse&lt;/a&gt;&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;Don’t get me wrong. I totally get its meaning, but at the same time do personal Matrix servers or &lt;a href=&#34;https://help.nextcloud.com/t/nextcloud-and-sqlite/34304&#34; target=&#34;_blank&#34; rel=&#34;noopener noreferrer&#34; &gt;home Nextcloud servers&lt;/a&gt; not count as “production”?&lt;/p&gt;
&lt;p&gt;&lt;a href=&#34;https://levels.io&#34; target=&#34;_blank&#34; rel=&#34;noopener noreferrer&#34; &gt;Pieter Levels&lt;/a&gt; famously used SQLite to help drive revenues from some of his products to &lt;a href=&#34;https://www.nocsdegree.com/pieter-levels-learn-coding&#34; target=&#34;_blank&#34; rel=&#34;noopener noreferrer&#34; &gt;well over six-digit dollar values&lt;/a&gt;, and SQLite’s &lt;a href=&#34;https://www.sqlite.org/whentouse.html&#34; target=&#34;_blank&#34; rel=&#34;noopener noreferrer&#34; &gt;own ‘appropriate uses’ list&lt;/a&gt; explains where it can be useful:&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;SQLite works great as the database engine for most low to medium traffic websites (which is to say, most websites) - &lt;a href=&#34;https://www.sqlite.org/whentouse.html&#34; target=&#34;_blank&#34; rel=&#34;noopener noreferrer&#34; &gt;sqlite.org&lt;/a&gt;&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;Even if your site or service does eventually outgrow SQLite (which will be a nice problem to have), your application code will still be using SQL and so it should be relatively easy to migrate to something like &lt;a href=&#34;https://www.postgresql.org&#34; target=&#34;_blank&#34; rel=&#34;noopener noreferrer&#34; &gt;PostgreSQL&lt;/a&gt;.&lt;/p&gt;
&lt;p&gt;As &lt;a href=&#34;http://paulgraham.com/ds.html&#34; target=&#34;_blank&#34; rel=&#34;noopener noreferrer&#34; &gt;Paul Graham said&lt;/a&gt;, “do things that don’t scale”.&lt;/p&gt;
&lt;p&gt;Of course, it is backed by disk and so is subject to the usual I/O constraints applicable to any file, but nearly all VPS providers offer SSD-backed instances these days and SQLite &lt;a href=&#34;https://sqlite.org/fasterthanfs.html&#34; target=&#34;_blank&#34; rel=&#34;noopener noreferrer&#34; &gt;claims to be faster than filesystem I/O&lt;/a&gt; anyway.&lt;/p&gt;
&lt;p&gt;It’s worth remembering that there can be huge overheads and costs in setting up “production-ready” database servers. You’ll need to think about provisioning the instance itself, installation of dependencies, certificates, the usual networking hardening (firewalls, ports, etc.) - and then keeping all of this up-to-date too. Even when using managed database services there are still user roles, authentication and rotating credentials to worry about, along with securely provisioning your applications with the connection strings.&lt;/p&gt;
&lt;p&gt;Having all of these things to worry about carries the additional risk of encouraging people to become lazy or to not have the time needed to make sure everything is done properly; an easy way to accidentally introduce security issues. Plus, if you have multiple environments (e.g. for staging or testing) then these factors, and the associated costs, amplify.&lt;/p&gt;
&lt;p&gt;There is also some interesting discussion on the topic in this &lt;a href=&#34;https://news.ycombinator.com/item?id=23281994&#34; target=&#34;_blank&#34; rel=&#34;noopener noreferrer&#34; &gt;Hacker News thread&lt;/a&gt; from last year.&lt;/p&gt;
&lt;p&gt;I just think it’s definitely worth a go before jumping straight into alternative heavier options. It’s free, has a &lt;a href=&#34;https://sqlite.org/footprint.html&#34; target=&#34;_blank&#34; rel=&#34;noopener noreferrer&#34; &gt;smaller footprint&lt;/a&gt;, has easily accessible bindings for many languages, and you can get started in minutes - &lt;a href=&#34;https://sqlite.org/onefile.html&#34; target=&#34;_blank&#34; rel=&#34;noopener noreferrer&#34; &gt;all you need is a file&lt;/a&gt;.&lt;/p&gt;
</content:encoded>
    </item>
    
    <item>
      <title>Dirty Little Secrets by Jo Spain</title>
      <link>https://wilw.dev/blog/2021/01/31/dirty-little-secrets/</link>
      <pubDate>Sun, 31 Jan 2021 18:15:00 +0000</pubDate>
      
      <guid>https://wilw.dev/blog/2021/01/31/dirty-little-secrets/</guid>
      
        <category>100daystooffload</category>
      
        <category>book</category>
      
      
      <content:encoded>&lt;p&gt;Recently I finished reading &lt;a href=&#34;https://www.goodreads.com/book/show/38120306-dirty-little-secrets&#34; target=&#34;_blank&#34; rel=&#34;noopener noreferrer&#34; &gt;Dirty Little Secrets&lt;/a&gt;. This is the first book I have read by &lt;a href=&#34;https://www.goodreads.com/author/show/14190033.Jo_Spain&#34; target=&#34;_blank&#34; rel=&#34;noopener noreferrer&#34; &gt;Jo Spain&lt;/a&gt; and the first time I have known of the author.&lt;/p&gt;
&lt;p&gt;&lt;img src=&#34;https://wilw.dev/media/blog/dirtylittlesecrets.jpg&#34; alt=&#34;Dirty Little Secrets cover&#34;&gt;&lt;/p&gt;
&lt;p&gt;The book first appears as though it’s a typical murder mystery set in a relatively wealthy gated community in Ireland - however the intricacies of the characters and narrative quickly made it hard to put down. The story begins with the discovery of the long-dead body of the woman who lives at number 4 and continues with the involvement of the detectives as they investigate the strange incident.&lt;/p&gt;
&lt;p&gt;The narrative primarily focuses on and is told from the perspectives of the neighbours and the police. It becomes clear that everyone - including the detectives - has hidden backgrounds and the story cleverly interwines past and present timelines (along with later repeated scenes told from different viewpoints) such that open-ended questions and arcs are often eventually resolved.&lt;/p&gt;
&lt;p&gt;I really enjoyed this book, which I listened to as an audiobook well narrated by Michele Moran. It helps reinforce the reality that everyone has obscured backgrounds or secret parts to them, which can be prematurely forced into the open by external events.&lt;/p&gt;
&lt;p&gt;Interestingly, another recent book I read - &lt;a href=&#34;https://www.goodreads.com/book/show/51933429-the-guest-list&#34; target=&#34;_blank&#34; rel=&#34;noopener noreferrer&#34; &gt;The Guest List by Lucy Foley&lt;/a&gt; - is a similar murder mystery also set in Ireland (Goodread’s content recommender systems clearly working at their best). Although on paper it is similar (in terms of its location and multiple character-based perspectives) and it is similarly well-reviewed by others, I personally didn’t really enjoy it. &lt;em&gt;The Guest List&lt;/em&gt; is certainly more of a suspenseful “thriller” in the traditional sense (largely given its setting) and the conclusion is probably more shocking, so I am not surprised it received good reviews. However, I just found the story to be a little dull and the characters a bit uninteresting and un-relatable. Each to their own, but I felt Jo Spain’s storytelling and character development to be far more compelling.&lt;/p&gt;
</content:encoded>
    </item>
    
    <item>
      <title>Out with the Old: Moving to Gitea</title>
      <link>https://wilw.dev/blog/2021/01/30/moving-to-gitea/</link>
      <pubDate>Sat, 30 Jan 2021 15:31:00 +0000</pubDate>
      
      <guid>https://wilw.dev/blog/2021/01/30/moving-to-gitea/</guid>
      
        <category>selfhosted</category>
      
        <category>100daystooffload</category>
      
        <category>selfhost</category>
      
        <category>gitea</category>
      
        <category>github</category>
      
        <category>analysis</category>
      
        <category>technology</category>
      
        <category>opinion</category>
      
      
      <content:encoded>&lt;p&gt;If you’ve visited my geminispace (&lt;a href=&#34;gemini://wilw.capsule.town&#34;&gt;gemini://wilw.capsule.town&lt;/a&gt;) you’ll have noticed that I’ve recently been on a mission to decentralise the every-day tools and services I use, and will understand the reasons why. This post will likely become part of a series of posts in which I talk about taking control and responsibility for my own data.&lt;/p&gt;
&lt;p&gt;One of the changes I’ve made more recently is to move many of my own personal projects (including the &lt;a href=&#34;https://git.wilw.dev/wilw/wilw.dev&#34; target=&#34;_blank&#34; rel=&#34;noopener noreferrer&#34; &gt;source for this site&lt;/a&gt;) over to a self-hosted &lt;a href=&#34;https://gitea.com&#34; target=&#34;_blank&#34; rel=&#34;noopener noreferrer&#34; &gt;Gitea&lt;/a&gt; service. I chose Gitea personally, but there are many other self-hosted solutions available (&lt;a href=&#34;https://www.paritybit.ca/blog/choosing-a-self-hosted-git-service&#34; target=&#34;_blank&#34; rel=&#34;noopener noreferrer&#34; &gt;see this post for examples and comparisons&lt;/a&gt;).&lt;/p&gt;
&lt;h3 id=&#34;the-problem-with-github&#34;&gt;The “problem” with GitHub&lt;/h3&gt;
&lt;p&gt;I’ve been a &lt;a href=&#34;https://github.com/willwebberley&#34; target=&#34;_blank&#34; rel=&#34;noopener noreferrer&#34; &gt;GitHub member&lt;/a&gt; for as long as I can remember, and will continue to be so and actively use it in my more professional work and when contributing to other projects. However, I don’t think I’m alone in that athough I try and develop things in the public and keep many home projects open-source, I &lt;strong&gt;usually&lt;/strong&gt; don’t do it with the &lt;em&gt;intention&lt;/em&gt; of receiving contributions from others. The discoverability on GitHub is great (though some may argue that its size means that things can get &lt;a href=&#34;https://slashdev.space/posts/2021-01-23-signal-to-noise&#34; target=&#34;_blank&#34; rel=&#34;noopener noreferrer&#34; &gt;a bit “diluted”&lt;/a&gt;), but many of the projects I develop are for my own use - and while anyone is free to take the code and use it as they want, the powerful tools offered by GitHub (and other centralised services) just never get used for these types of projects.&lt;/p&gt;
&lt;p&gt;The other thing is that GitHub seems to have gradually become the LinkedIn of the software world, and many people use it as the basis of their CV or portfolio. This is great as it allows other people and potential employers to get an idea of the kinds of things a developer works on, coding style, and so on, but there’s always a certain feel of &lt;em&gt;pressure&lt;/em&gt; (or sometimes subconscious competitiveness) that people on any socially-focused platforms might get.&lt;/p&gt;
&lt;p&gt;When Twitter introduced their Fleets feature they mentioned that one of the motivators behind the project is that they understand that some people &lt;a href=&#34;https://blog.twitter.com/en_us/topics/product/2020/introducing-fleets-new-way-to-join-the-conversation.html&#34; target=&#34;_blank&#34; rel=&#34;noopener noreferrer&#34; &gt;get a fear of posting tweets&lt;/a&gt; when things feel so public. I’ve seen the same thing with GitHub in that people feel put-off contributing or publishing their own work in public repositories “in case someone sees” - is this a barrier to entry for more introverted developers? Inversely, re-engagement mechanisms - like the contributions graph on each user’s profile - may make developers just publish for the sake of it.&lt;/p&gt;
&lt;p&gt;None of these things are necessarily problems or wrong (private repos are always an option, for example), but these days it just feels more appropriate to be responsible for your own data as much as possible - especially when not making the most of what alternatives can provide you with, and it’s always good to use and encourage alternative options so that one service doesn’t become the expected norm.&lt;/p&gt;
&lt;h3 id=&#34;my-experience-so-far&#34;&gt;My experience so far&lt;/h3&gt;
&lt;p&gt;Since migrating many projects over to the smaller “world” that is my own git server, I get the feel that things are slower (&lt;a href=&#34;https://jackcheng.com/essays/the-slow-web&#34; target=&#34;_blank&#34; rel=&#34;noopener noreferrer&#34; &gt;in a good way&lt;/a&gt;) and I have been spending more time curating projects and working on the things I actually want to work on (though many are still private “for now”!).&lt;/p&gt;
&lt;p&gt;If you’re interested in trying your own self-hosted Gitea server, then it’s pretty straight forward if you have a VPS (I just used the official Docker images, for which there are instructions &lt;a href=&#34;https://docs.gitea.io/en-us/install-with-docker&#34; target=&#34;_blank&#34; rel=&#34;noopener noreferrer&#34; &gt;in the documentation&lt;/a&gt;).&lt;/p&gt;
&lt;p&gt;To move existing repositories over it’s as simple as changing the &lt;code&gt;remote&lt;/code&gt; (or adding a new one) in your local git configuration for the project and then re-pushing. Gitea also includes a migration service to automatically pull repositories through, and can also be set-up to mirror other remote repos.&lt;/p&gt;
&lt;p&gt;In terms of performance, I’ve found it quick to use and navigate (certainly faster than GitHub’s web interface) on a $10 VPS from Linode that I had anyway and on which I host many other services too.&lt;/p&gt;
&lt;p&gt;It’s definitely worth a try if this is something you’re interested in. &lt;a href=&#34;https://fosstodon.org/@wilw&#34; target=&#34;_blank&#34; rel=&#34;noopener noreferrer&#34; &gt;Let me know&lt;/a&gt; how you get on.&lt;/p&gt;
</content:encoded>
    </item>
    
    <item>
      <title>100 Days to Offload Challenge</title>
      <link>https://wilw.dev/blog/2021/01/29/100-days-to-offload/</link>
      <pubDate>Fri, 29 Jan 2021 17:46:00 +0000</pubDate>
      
      <guid>https://wilw.dev/blog/2021/01/29/100-days-to-offload/</guid>
      
        <category>100daystooffload</category>
      
        <category>life</category>
      
      
      <content:encoded>&lt;p&gt;I know that I’ve been a bit crap at updating my blog properly and consistently over the past few years. One of my new year’s resolutions &lt;em&gt;this year&lt;/em&gt; is to get into the habit of writing more, and so &lt;a href=&#34;https://100daystooffload.com&#34; target=&#34;_blank&#34; rel=&#34;noopener noreferrer&#34; &gt;#100DaysToOffload&lt;/a&gt; seems a good opportunity to challenge myself to make sure I do.&lt;/p&gt;
&lt;p&gt;The guidelines for and the ideas behind the challenge are &lt;a href=&#34;https://100daystooffload.com&#34; target=&#34;_blank&#34; rel=&#34;noopener noreferrer&#34; &gt;on the challenge’s website&lt;/a&gt;. There aren’t any rules really, but the essential message is to “Just. Write.”. So, I’ll do my best before the end of 2021, and given that I’ve already published two posts this year I’ll count this &lt;em&gt;number 3&lt;/em&gt;.&lt;/p&gt;
&lt;p&gt;I will try to keep things tech-related as much as possible. There’s only so much there that I can write about though, so I will probably also include bits from my life, books I read, things I’ve watched, etc.&lt;/p&gt;
&lt;p&gt;If you want to follow along, you can &lt;a href=&#34;https://wilw.dev/rss.xml&#34;&gt;subscribe to my RSS feed&lt;/a&gt;. If you need an RSS reader, I can definitely recommend &lt;a href=&#34;https://www.reederapp.com&#34; target=&#34;_blank&#34; rel=&#34;noopener noreferrer&#34; &gt;Reeder 5&lt;/a&gt;, which is available for &lt;a href=&#34;https://itunes.apple.com/app/id1529448980&#34; target=&#34;_blank&#34; rel=&#34;noopener noreferrer&#34; &gt;macOS&lt;/a&gt; and &lt;a href=&#34;https://apps.apple.com/app/id1529445840&#34; target=&#34;_blank&#34; rel=&#34;noopener noreferrer&#34; &gt;iOS&lt;/a&gt; - it’s fab. If you’re trying the challenge too, then &lt;a href=&#34;https://fosstodon.org/@wilw&#34; target=&#34;_blank&#34; rel=&#34;noopener noreferrer&#34; &gt;let me know&lt;/a&gt; so I can check out your posts!&lt;/p&gt;
</content:encoded>
    </item>
    
    <item>
      <title>Project Gemini</title>
      <link>https://wilw.dev/blog/2021/01/20/project-gemini/</link>
      <pubDate>Wed, 20 Jan 2021 17:00:00 +0000</pubDate>
      
      <guid>https://wilw.dev/blog/2021/01/20/project-gemini/</guid>
      
        <category>100daystooffload</category>
      
        <category>gemini</category>
      
        <category>technology</category>
      
      
      <content:encoded>&lt;p&gt;Over the past few months I have been trying to use centralised “big tech” social media platforms less and instead immerse myself into the more community-driven “fediverse” of decentralised services that are connected (“federated”) using common protocols (e.g. &lt;a href=&#34;https://en.wikipedia.org/wiki/ActivityPub&#34; target=&#34;_blank&#34; rel=&#34;noopener noreferrer&#34; &gt;ActivityPub&lt;/a&gt;). If you like, you can follow me on Mastodon (&lt;a href=&#34;https://fosstodon.org/@wilw&#34; target=&#34;_blank&#34; rel=&#34;noopener noreferrer&#34; &gt;@wilw@fosstodon.org&lt;/a&gt;, recently migrated over from my &lt;a href=&#34;https://mastodon.social/@will88&#34; target=&#34;_blank&#34; rel=&#34;noopener noreferrer&#34; &gt;old mastodon.social account&lt;/a&gt;) and Pixelfed (&lt;a href=&#34;https://pixelfed.social/@wilw&#34; target=&#34;_blank&#34; rel=&#34;noopener noreferrer&#34; &gt;@wilw@pixelfed.social&lt;/a&gt;).&lt;/p&gt;
&lt;p&gt;I’ve loved spending my time on these platforms - mainly due to the lack of noise and fuss, and more of a focus on sharing relevant content and interesting interactions with likeminded people (though of course this does depend on the &lt;a href=&#34;https://joinmastodon.org&#34; target=&#34;_blank&#34; rel=&#34;noopener noreferrer&#34; &gt;instance you join&lt;/a&gt;).&lt;/p&gt;
&lt;p&gt;One of the things I’ve seen talked about more and more is &lt;a href=&#34;https://gemini.circumlunar.space&#34; target=&#34;_blank&#34; rel=&#34;noopener noreferrer&#34; &gt;Gemini&lt;/a&gt; - and having learned about it and participated myself - I have come to love the ideas behind it.&lt;/p&gt;
&lt;p&gt;Some people will remember the &lt;a href=&#34;https://en.wikipedia.org/wiki/Gopher_%28protocol%29&#34; target=&#34;_blank&#34; rel=&#34;noopener noreferrer&#34; &gt;Gopher protocol&lt;/a&gt; - a lighter alternative to the web that was ultimately sidelined by most of the world in favour of the HTTP World-Wide Web in the early 90s. The &lt;a href=&#34;https://en.wikipedia.org/wiki/Gemini_%28protocol%29&#34; target=&#34;_blank&#34; rel=&#34;noopener noreferrer&#34; &gt;Gemini protocol&lt;/a&gt; is newer, having started in 2019, but is inspired by Gopher. In particular, it aims to solve some of the problems experienced by the modern HTTP(S) web we know today - around complexity, privacy, and “bloat” - and focuses on providing a graph of usefully connected &lt;em&gt;content&lt;/em&gt;.&lt;/p&gt;
&lt;p&gt;Gemini “sites” (known as “capsules” or “stars”/“starships”) - the resources that form &lt;a href=&#34;https://en.wikipedia.org/wiki/Gemini_space&#34; target=&#34;_blank&#34; rel=&#34;noopener noreferrer&#34; &gt;Geminispace&lt;/a&gt; - are located using the &lt;code&gt;gemini://&lt;/code&gt; URL scheme. Servers typically listen on port 1965: a homage to the &lt;a href=&#34;https://en.wikipedia.org/wiki/Project_Gemini&#34; target=&#34;_blank&#34; rel=&#34;noopener noreferrer&#34; &gt;NASA’s Gemini Project&lt;/a&gt;. Gemini text resources are similar to traditional HTML web pages in the sense that they can include links to other resources and provide structure and hierarchy through a markdown-like syntax. All Gemini resources must also be transferred using TLS.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;The image below shows my own capsule (found at &lt;a href=&#34;gemini://wilw.capsule.town&#34;&gt;gemini://wilw.capsule.town&lt;/a&gt;). If you can’t open that link yet then read to the end of this post.&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;&lt;img src=&#34;https://wilw.dev/media/blog/amfora.png&#34; alt=&#34;My Gem Capsule running in the Amfora client&#34;&gt;&lt;/p&gt;
&lt;p&gt;However, there are also significant differences. These &lt;code&gt;.gmi&lt;/code&gt; files (and files with similar extensions served with the &lt;code&gt;text/gemini&lt;/code&gt; MIME type) cannot indicate any styling instructions (as HTML often does with CSS) - and instead leaves the display and rendering of the file up to the client. In addition, whilst images can be served over the protocol, they cannot be included in (and rendered within) Gemini text files like they can in HTML. Similarly, there is no client-side dynamicity for these resources, such as the JavaScript included with most HTML web pages. It’s simple; the client just renders whatever is provided by the server, and that’s it.&lt;/p&gt;
&lt;p&gt;The simplicity of the protocol - without styling, embedded images, client-side scripts, and more - offers a lightweight, ad-free, and content-oriented experience that is also available for low-powered devices and machines on slower networks. There is more of a focus on privacy (servers can’t track or fingerprint you beyond knowing your IP address), and the relative “smallness” of it and absence of big-tech presence certainly brings back some of the fun and novelty of the early web as we remember it.&lt;/p&gt;
&lt;p&gt;I certainly recommend visiting the &lt;a href=&#34;https://gemini.circumlunar.space&#34; target=&#34;_blank&#34; rel=&#34;noopener noreferrer&#34; &gt;project’s website&lt;/a&gt; for more useful information.&lt;/p&gt;
&lt;h2 id=&#34;getting-involved&#34;&gt;Getting involved&lt;/h2&gt;
&lt;p&gt;Since Gemini resources are not served using HTTP(S), you can’t access them using a normal web browser (although you can use HTTP proxies such as &lt;a href=&#34;https://portal.mozz.us/gemini/wilw.capsule.town&#34; target=&#34;_blank&#34; rel=&#34;noopener noreferrer&#34; &gt;Mozz’s Portal&lt;/a&gt;).&lt;/p&gt;
&lt;p&gt;Instead, you’ll need a Gemini client. I use &lt;a href=&#34;https://github.com/makeworld-the-better-one/amfora&#34; target=&#34;_blank&#34; rel=&#34;noopener noreferrer&#34; &gt;Amfora&lt;/a&gt; on my Mac and the &lt;a href=&#34;https://apps.apple.com/gb/app/the-gemini-browser/id1514950389&#34; target=&#34;_blank&#34; rel=&#34;noopener noreferrer&#34; &gt;Gemini Browser&lt;/a&gt; on my phone.&lt;/p&gt;
&lt;p&gt;Once you have a client, you can view my own capsule by visiting &lt;a href=&#34;gemini://wilw.capsule.town&#34;&gt;gemini://wilw.capsule.town&lt;/a&gt;. If you’re interested in starting your own and want to see how mine is formed, you can view the &lt;a href=&#34;https://git.wilw.dev/wilw/gemini-capsule&#34; target=&#34;_blank&#34; rel=&#34;noopener noreferrer&#34; &gt;project source&lt;/a&gt;.&lt;/p&gt;
&lt;p&gt;I also recommend trying out the Gemini search-engine to discover what else lies in Geminispace. It is located at &lt;a href=&#34;gemini://gus.guru&#34;&gt;gemini://gus.guru&lt;/a&gt;.&lt;/p&gt;
</content:encoded>
    </item>
    
    <item>
      <title>Scaling serverless apps: some lessons learned</title>
      <link>https://wilw.dev/blog/2021/01/03/scaling-serverless/</link>
      <pubDate>Sun, 03 Jan 2021 17:00:00 +0000</pubDate>
      
      <guid>https://wilw.dev/blog/2021/01/03/scaling-serverless/</guid>
      
        <category>100daystooffload</category>
      
        <category>serverless</category>
      
        <category>aws</category>
      
        <category>lambda</category>
      
        <category>analysis</category>
      
        <category>technology</category>
      
        <category>opinion</category>
      
      
      <content:encoded>&lt;p&gt;Building apps on serverless architecture has been a game-changer for me and for developers everywhere, enabling small dev teams to cheaply build and scale services from MVP through to enterprise deployment.&lt;/p&gt;
&lt;p&gt;Taking advantage of serverless solutions - such as AWS’ Lambda, Google’s Cloud Functions, and Cloudflare’s Workers - means less resource is spent on traditional dev-ops and deployment and, especially when combined with tools like &lt;a href=&#34;https://www.serverless.com&#34; target=&#34;_blank&#34; rel=&#34;noopener noreferrer&#34; &gt;Serverless framework&lt;/a&gt; and its rich &lt;a href=&#34;https://www.serverless.com/plugins&#34; target=&#34;_blank&#34; rel=&#34;noopener noreferrer&#34; &gt;ecosystem of plugins&lt;/a&gt;, you can use the time instead to better develop your products. Let the provider worry about deploying your code, keeping your services highly available, and scaling them to meet the needs of huge audiences.&lt;/p&gt;
&lt;p&gt;Serverless providers often &lt;a href=&#34;https://aws.amazon.com/lambda/pricing&#34; target=&#34;_blank&#34; rel=&#34;noopener noreferrer&#34; &gt;bill by&lt;/a&gt; number of “invocations” (the number of times your functions are executed) and total execution time. This means you’re only paying for your services while they are actively being used, as opposed to the traditional server(-full?) model in which you have to pay to keep your services available 24/7. Given providers often have generous free tiers, you might not even start paying for anything until your service gets popular enough, which is always a nice problem to have.&lt;/p&gt;
&lt;p&gt;However, before you get to that stage, it’s worth also being aware of some of the pitfalls that come with adopting a serverless-only approach and knowing you may later need to prepare to migrate your service further down the line. Many of these are by design; to help keep things quick to deploy and run, but others are less obvious or predictable. I’ll refer to AWS and Lambda (and Serverless framework) in this post, as it’s what I’m most familiar with, but the concepts should mostly translate to other providers.&lt;/p&gt;
&lt;h2 id=&#34;it-might-not-stay-cheap&#34;&gt;It might not stay cheap&lt;/h2&gt;
&lt;p&gt;If deploying a web service to Lambda, it’s likely you’ll front it using &lt;a href=&#34;https://aws.amazon.com/api-gateway&#34; target=&#34;_blank&#34; rel=&#34;noopener noreferrer&#34; &gt;API Gateway&lt;/a&gt;, and Serverless framework will set this all up for you at deploy-time. Whilst offering lots of nice features (authorizers, automatic TLS termination, integrations with &lt;a href=&#34;https://aws.amazon.com/certificate-manager&#34; target=&#34;_blank&#34; rel=&#34;noopener noreferrer&#34; &gt;Certificate Manager&lt;/a&gt;, and more), API Gateway also comes with a nice free tier.&lt;/p&gt;
&lt;p&gt;However, you will &lt;a href=&#34;https://aws.amazon.com/api-gateway/pricing&#34; target=&#34;_blank&#34; rel=&#34;noopener noreferrer&#34; &gt;start paying&lt;/a&gt; once your service gets popular enough. This means that on top of Lambda billing you for invocations and compute time, you’re also getting hit by bandwidth and API call costs. If by good (or bad) marketing your service ends up suddenly getting a lot of visitors - perhaps whilst you’re away for a week on holiday - AWS will happily and seamlessly scale to meet the demand, but you might get hit with a nasty bill on your return.&lt;/p&gt;
&lt;p&gt;There are a number of posts online on this matter (&lt;a href=&#34;https://einaregilsson.com/serverless-15-percent-slower-and-eight-times-more-expensive&#34; target=&#34;_blank&#34; rel=&#34;noopener noreferrer&#34; &gt;example here&lt;/a&gt;), so it’s worth doing some research beforehand.&lt;/p&gt;
&lt;h2 id=&#34;performance&#34;&gt;Performance&lt;/h2&gt;
&lt;p&gt;The link mentioned just above also makes note of the performance of Lambda functions. Whilst the technology makes development and delivery super easy, it comes with the downside of reduced performance.&lt;/p&gt;
&lt;p&gt;To save costs Lambda keeps your function code out of memory. It will only provision compute power, locate and retrieve your code, and finally execute it when the function is actually invoked. This is known as a &lt;a href=&#34;https://www.serverless.com/blog/keep-your-lambdas-warm&#34; target=&#34;_blank&#34; rel=&#34;noopener noreferrer&#34; &gt;cold start&lt;/a&gt;, and invocations that start cold will cause your requests to take longer. If the function is executed frequently, Lambda will keep it “warm” and you’ll notice the requests are quicker.&lt;/p&gt;
&lt;p&gt;Note that cold starts &lt;a href=&#34;https://blog.cloudflare.com/eliminating-cold-starts-with-cloudflare-workers&#34; target=&#34;_blank&#34; rel=&#34;noopener noreferrer&#34; &gt;may not be a problem for Cloudflare Workers&lt;/a&gt; and Serverless framework has &lt;a href=&#34;https://github.com/juanjoDiaz/serverless-plugin-warmup&#34; target=&#34;_blank&#34; rel=&#34;noopener noreferrer&#34; &gt;plugins to help mitigate them too&lt;/a&gt;.&lt;/p&gt;
&lt;p&gt;AWS Lambda also has other overheads which may mean you notice a performance reduction compared to when running your app in a container or on a VM.&lt;/p&gt;
&lt;h2 id=&#34;limitations&#34;&gt;Limitations&lt;/h2&gt;
&lt;p&gt;Lambda imposes a number of limitations that you would not be subject to when deploying using a VM or containers. Many can be increased, but this would come with cost implications and require a chat with AWS support.&lt;/p&gt;
&lt;h2 id=&#34;language-runtimes&#34;&gt;Language runtimes&lt;/h2&gt;
&lt;p&gt;Historically, Lambda only offered a fixed set of language runtimes and versions - including some of the most popular, such as Python, Node, Ruby, and Go. You can now provide your own runtime (e.g. if you wanted to write a service in Rust), but this requires you to do &lt;a href=&#34;https://docs.aws.amazon.com/lambda/latest/dg/runtimes-custom.html&#34; target=&#34;_blank&#34; rel=&#34;noopener noreferrer&#34; &gt;your own configuration&lt;/a&gt; first.&lt;/p&gt;
&lt;h2 id=&#34;function-file-size&#34;&gt;Function file size&lt;/h2&gt;
&lt;p&gt;AWS has a default limit of 75GB for storing all of your Lambda functions, dependencies, and layers. This seems like quite a lot, but if you had multiple NodeJS Lambda applications, each with large &lt;code&gt;node_modules&lt;/code&gt; directories for dependencies, and considering Serverless framework &lt;a href=&#34;https://www.serverless.com/framework/docs/providers/aws/guide/functions#versioning-deployed-functions&#34; target=&#34;_blank&#34; rel=&#34;noopener noreferrer&#34; &gt;versions each release&lt;/a&gt; by default, you might hit this limit sooner than you think and become unable to release any new code until you can find something to delete.&lt;/p&gt;
&lt;h2 id=&#34;request-and-response-data&#34;&gt;Request and response data&lt;/h2&gt;
&lt;p&gt;When using a Lambda function as part of a web service, it will naturally be involved with handling request data and emitting response data. This is one that got me confused for a while in an API I was building: there is a 6MB limit to the size of the request and response.&lt;/p&gt;
&lt;p&gt;Whilst this won’t (and shouldn’t) be an issue for most REST APIs, a client for a project I worked on needed a large volume of data returned by a single call for daily automated data exports, which one day just stopped working by itself. Cloudwatch logs didn’t reveal any particular problems and API Gateway was only responding with non-descriptive errors. After some investigation it turned out that the response had trickled over the payload size limit. It’s worth considering and remembering this if you ever get hit with a similar problem.&lt;/p&gt;
&lt;h2 id=&#34;response-structure&#34;&gt;Response structure&lt;/h2&gt;
&lt;p&gt;Remember that if your Lambda function does not return a correctly-formatted response, API Gateway will respond to the client with an error status code - even if the function executed successfully. Always return a response from your function since Cloudwatch won’t give much away in this scenario either (the function may have still executed successfully), which is another pain to debug.&lt;/p&gt;
&lt;h2 id=&#34;resource-limits&#34;&gt;Resource limits&lt;/h2&gt;
&lt;p&gt;When deploying an application using Serverless framework, a number of AWS resources are created on your behalf in a single “stack” using &lt;a href=&#34;https://aws.amazon.com/cloudformation&#34; target=&#34;_blank&#34; rel=&#34;noopener noreferrer&#34; &gt;CloudFormation&lt;/a&gt;. On the surface, a single function could be associated with a large number of “resources” (e.g. the function itself, layers, IAM roles, Cloudwatch log groups, authorizers, API Gateway resources/methods, etc.).&lt;/p&gt;
&lt;p&gt;CloudFormation limits stacks to 500 resources each, and if your application has more than a handful of functions (e.g. for handling different types of events) then you might very well this limit.&lt;/p&gt;
&lt;p&gt;It can be mitigated by combining similar functions together (using arguments to define any differences), or by separating functions out to another Serverless application. Either way, you’d need to consider your strategy with this.&lt;/p&gt;
&lt;h2 id=&#34;closing-remarks&#34;&gt;Closing remarks&lt;/h2&gt;
&lt;p&gt;Despite some of these fallbacks, I can still very much recommend taking advantage of serverless architecture when creating your apps, and I will certainly continue to build on these platforms in many areas of my work.&lt;/p&gt;
&lt;p&gt;The purpose of this post is just to raise awareness around some of the invonveniences I have faced during my time working with this technology, but I still love its power and flexibility.&lt;/p&gt;
</content:encoded>
    </item>
    
    <item>
      <title>React Query</title>
      <link>https://wilw.dev/blog/2020/12/15/react-query/</link>
      <pubDate>Tue, 15 Dec 2020 17:00:00 +0000</pubDate>
      
      <guid>https://wilw.dev/blog/2020/12/15/react-query/</guid>
      
        <category>javascript</category>
      
        <category>react</category>
      
        <category>webapi</category>
      
        <category>technology</category>
      
      
      <content:encoded>&lt;p&gt;If you write React web apps that interface with a backend web API then definitely consider trying &lt;a href=&#34;https://react-query.tanstack.com&#34; target=&#34;_blank&#34; rel=&#34;noopener noreferrer&#34; &gt;React Query&lt;/a&gt;.&lt;/p&gt;
&lt;p&gt;The library makes use of modern React patterns, such as hooks, to keep code concise and readable. It probably means you can keep API calls directly inside your normal component code rather than setting-up your own client-side API interface modules.&lt;/p&gt;
&lt;p&gt;React Query will also cache resolved data through unique “query keys”, so you can keep transitions in UIs fast with cached data without needing to rely on redux.&lt;/p&gt;
&lt;p&gt;You’ll still need to write the actual requests yourself (e.g. using &lt;a href=&#34;https://developer.mozilla.org/en-US/docs/Web/API/Fetch_API&#34; target=&#34;_blank&#34; rel=&#34;noopener noreferrer&#34; &gt;fetch&lt;/a&gt; or &lt;a href=&#34;https://github.com/axios/axios&#34; target=&#34;_blank&#34; rel=&#34;noopener noreferrer&#34; &gt;axios&lt;/a&gt;) but using this certainly takes away a lot of the pain in building smooth asynchronous web apps.&lt;/p&gt;
&lt;p&gt;To try it out install it with your usual Node package manager and begin with the &lt;a href=&#34;https://react-query.tanstack.com/quick-start&#34; target=&#34;_blank&#34; rel=&#34;noopener noreferrer&#34; &gt;quick start guide&lt;/a&gt;.&lt;/p&gt;
</content:encoded>
    </item>
    
    <item>
      <title>JS Tidbit: Nullish Coalescing</title>
      <link>https://wilw.dev/blog/2020/11/20/nullish-coalescing/</link>
      <pubDate>Fri, 20 Nov 2020 17:00:00 +0000</pubDate>
      
      <guid>https://wilw.dev/blog/2020/11/20/nullish-coalescing/</guid>
      
        <category>javascript</category>
      
        <category>technology</category>
      
      
      <content:encoded>&lt;p&gt;This short post introduces a useful JavaScript operator to help make your one-liners even more concise.&lt;/p&gt;
&lt;p&gt;&lt;a href=&#34;https://tc39.es/ecma262/#prod-CoalesceExpression&#34; target=&#34;_blank&#34; rel=&#34;noopener noreferrer&#34; &gt;The specification&lt;/a&gt; was added formally in the 11th edition of ECMAScript. It is implemented as a logical operator to selectively return the result of one of two expressions (or operands) based on one of the expressions resolving to a “nullish” value. A nullish value in JavaScript is one that is &lt;code&gt;null&lt;/code&gt; or &lt;code&gt;undefined&lt;/code&gt;.&lt;/p&gt;
&lt;p&gt;In particular, the operator - given by &lt;code&gt;??&lt;/code&gt; - will return the right-hand side if the left-hand expression resolves to &lt;code&gt;null&lt;/code&gt; or &lt;code&gt;undefined&lt;/code&gt;, and otherwise returns the left-hand side.&lt;/p&gt;
&lt;pre tabindex=&#34;0&#34;&gt;&lt;code&gt;const x = y ?? z;
&lt;/code&gt;&lt;/pre&gt;&lt;p&gt;In the example above, &lt;code&gt;z&lt;/code&gt; will be returned if &lt;code&gt;y&lt;/code&gt; is “nullish”, and otherwise  &lt;code&gt;y&lt;/code&gt; will be returned.&lt;/p&gt;
&lt;p&gt;Nullish coalescing is similar to (but stricter than) the more commonly-seen logical OR operator - given by &lt;code&gt;||&lt;/code&gt; - which returns the result of the right-hand side expression if the left-hand side resolves to any falsy value, which includes nullish ones in addition to the boolean &lt;code&gt;false&lt;/code&gt;, empty string (&lt;code&gt;&#39;&#39;&lt;/code&gt;), &lt;code&gt;0&lt;/code&gt;,  &lt;code&gt;NaN&lt;/code&gt;, etc.&lt;/p&gt;
&lt;p&gt;More info is &lt;a href=&#34;https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Operators/Nullish_coalescing_operator&#34; target=&#34;_blank&#34; rel=&#34;noopener noreferrer&#34; &gt;available on MDN&lt;/a&gt; and Wikipedia discusses null coalescing in &lt;a href=&#34;https://en.wikipedia.org/wiki/Null_coalescing_operator&#34; target=&#34;_blank&#34; rel=&#34;noopener noreferrer&#34; &gt;other languages&lt;/a&gt;.&lt;/p&gt;
</content:encoded>
    </item>
    
    <item>
      <title>JS Tidbit: Optional Chaining</title>
      <link>https://wilw.dev/blog/2020/10/10/optional-chaining/</link>
      <pubDate>Sat, 10 Oct 2020 17:00:00 +0000</pubDate>
      
      <guid>https://wilw.dev/blog/2020/10/10/optional-chaining/</guid>
      
        <category>javascript</category>
      
        <category>technology</category>
      
      
      <content:encoded>&lt;p&gt;JavaScript has lots of handy tools for creating concise code and one-liners. One such tool is the optional chaining operator.&lt;/p&gt;
&lt;p&gt;The optional chaining operator is useful for addressing an attribute of a deeply-nested object in which you cannot be fully certain that the successive levels of the object are valid at run-time.&lt;/p&gt;
&lt;p&gt;For example, consider the following object.&lt;/p&gt;
&lt;pre tabindex=&#34;0&#34;&gt;&lt;code&gt;const person = {
  name: &#39;Harry&#39;,
  occupation: &#39;student&#39;,
  enrolmentInformation: {
    contactDetails: {
      email: &#39;harry@hogwarts.ac.uk&#39;,
      address: {
        firstLine: &#39;4 Privet Drive&#39;,
        postCode: &#39;GU3 4GH&#39;
      }
    }
  }
};
&lt;/code&gt;&lt;/pre&gt;&lt;p&gt;In order to &lt;strong&gt;safely&lt;/strong&gt; (i.e. if you cannot guarantee each object level at run-time) read the nested &lt;code&gt;postCode&lt;/code&gt; attribute, you could do so like this, using the logical AND operator:&lt;/p&gt;
&lt;pre tabindex=&#34;0&#34;&gt;&lt;code&gt;const enrolmentInfo = person &amp;&amp; person.enrolmentInformation;
const contactDetails = enrolmentInfo &amp;&amp; enrolmentInfo.contactDetails;
const address = contactDetails &amp;&amp; contactDetails.address;
const postCode = address ^^ address.postCode;
&lt;/code&gt;&lt;/pre&gt;&lt;p&gt;Even in its single-line equivalent, this very quickly gets un-readable and messy.&lt;/p&gt;
&lt;p&gt;The optional chaining operator (&lt;code&gt;?.&lt;/code&gt;) makes this much easier:&lt;/p&gt;
&lt;pre tabindex=&#34;0&#34;&gt;&lt;code&gt;const postCode = person?.enrolmentInformation?.contactDetails?.address?.postCode;
&lt;/code&gt;&lt;/pre&gt;&lt;p&gt;Or, using object destructuring and a logical OR:&lt;/p&gt;
&lt;pre tabindex=&#34;0&#34;&gt;&lt;code&gt;const { postCode } = person?.enrolmentInformation?.contactDetails?.address || {};
&lt;/code&gt;&lt;/pre&gt;&lt;p&gt;Essentially, at each successive level, the optional chaining operator checks that the current attribute is non-nullish (i.e. not &lt;code&gt;null&lt;/code&gt; or &lt;code&gt;undefined&lt;/code&gt;) before proceeding to the next. If it does try to access a nullish attribute, the expression short-circuits and returns &lt;code&gt;undefined&lt;/code&gt;, ignoring the rest of the chain.&lt;/p&gt;
&lt;p&gt;Optional chaining also works in other scenarios:&lt;/p&gt;
&lt;pre tabindex=&#34;0&#34;&gt;&lt;code&gt;const x = doubleArray[3]?.toFixed(2); // accessing array elements
const y = maybeArray?.[0]; // accessing an array that may exist
car.openDoors?.(); // calling methods on nested objects
&lt;/code&gt;&lt;/pre&gt;&lt;p&gt;For more info, see the &lt;a href=&#34;https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Operators/Optional_chaining&#34; target=&#34;_blank&#34; rel=&#34;noopener noreferrer&#34; &gt;MDN reference&lt;/a&gt;.&lt;/p&gt;
</content:encoded>
    </item>
    
    <item>
      <title>Command-line bookkeeping in Animal Crossing</title>
      <link>https://wilw.dev/blog/2020/05/23/command-line-bookkeeping-acnh/</link>
      <pubDate>Sat, 23 May 2020 17:00:00 +0000</pubDate>
      
      <guid>https://wilw.dev/blog/2020/05/23/command-line-bookkeeping-acnh/</guid>
      
        <category>ledger</category>
      
        <category>finance</category>
      
        <category>technology</category>
      
      
      <content:encoded>&lt;p&gt;I recently stumbled across &lt;a href=&#34;https://www.csun.io/2020/05/17/gnucash-finance.html&#34; target=&#34;_blank&#34; rel=&#34;noopener noreferrer&#34; &gt;an article&lt;/a&gt; on Hacker News discussing the pros of basic personal accounting using &lt;a href=&#34;https://www.gnucash.org/&#34; target=&#34;_blank&#34; rel=&#34;noopener noreferrer&#34; &gt;GnuCash&lt;/a&gt; - a free and open-source desktop accounting program. The article was interesting as the data geek in me resonated with the notion of being able to query the information in useful ways, particularly after having used the system for enough time to accumulate enough financial data.&lt;/p&gt;
&lt;p&gt;The &lt;a href=&#34;https://news.ycombinator.com/item?id=23237445&#34; target=&#34;_blank&#34; rel=&#34;noopener noreferrer&#34; &gt;comments on the article’s post&lt;/a&gt; also mentioned another tool, &lt;a href=&#34;https://www.ledger-cli.org/&#34; target=&#34;_blank&#34; rel=&#34;noopener noreferrer&#34; &gt;Ledger&lt;/a&gt;. Whilst GnuCash allows users to input transactional and account information as well as reports, Ledger’s focus is only on the reports - a key feature of this CLI tool is that the actual bookkeeping is made directly (or through other tools) into a text file, which Ledger only reads from and never otherwise touches. Both programs work on the principle of &lt;a href=&#34;https://en.wikipedia.org/wiki/Double-entry_bookkeeping&#34; target=&#34;_blank&#34; rel=&#34;noopener noreferrer&#34; &gt;double-entry bookkeeping&lt;/a&gt;, but some of the key positives of Ledger are its speed (&lt;a href=&#34;https://www.ledger-cli.org/3.0/doc/ledger3.html#Archiving-Previous-Years&#34; target=&#34;_blank&#34; rel=&#34;noopener noreferrer&#34; &gt;even when working with several decades’ worth of financial data&lt;/a&gt;) and its innate ability to be combined with other useful UNIX tools - both for data input and, if necessary (Ledger’s own reporting outputs are very powerful), output.&lt;/p&gt;
&lt;p&gt;The fact that it relies on only a single human-manageable text file, which can contain the data for any number of accounts, and which can easily be version-controlled, is also a bonus in my eyes. The author of &lt;a href=&#34;http://furius.ca/beancount/&#34; target=&#34;_blank&#34; rel=&#34;noopener noreferrer&#34; &gt;Beancount&lt;/a&gt; - a similar program worth a look at - also wrote a good article about the benefits of &lt;a href=&#34;https://docs.google.com/document/d/1e4Vz3wZB_8-ZcAwIFde8X5CjzKshE4-OXtVVHm4RQ8s&#34; target=&#34;_blank&#34; rel=&#34;noopener noreferrer&#34; &gt;command-line accounting&lt;/a&gt;.&lt;/p&gt;
&lt;p&gt;I do not come from an accounting or bookkeeping background, but I was keen to start trying it out for myself, and wanted to make sure that I could get the grips of the basics before diving in too far and making too many mistakes that I’d have to go back and change later. I currently use &lt;a href=&#34;https://www.xero.com/uk/&#34; target=&#34;_blank&#34; rel=&#34;noopener noreferrer&#34; &gt;Xero&lt;/a&gt; for some business work, which happily handles (nearly) everything for me - invoicing, payments, automatic bank feeds and reconciliations, and more. It sometimes feels a bit slow and clunky, though, and its general ease of use does teach much about what actually goes on behind the scenes. Ledger seemed like a great alternative to use for personal finance - a powerful tool for viewing key information at a glance, combined with a familiar and standard method to “input” data. It also gives me the feel that I am more intimate with my accounts and also a richer understanding of my financial model.&lt;/p&gt;
&lt;p&gt;Moving forwards from my initial experiments, I had three main aims:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;Learn the basics of bookkeeping (specifically with Ledger in mind);&lt;/li&gt;
&lt;li&gt;Learn how to run reports on “the books” using Ledger (e.g. profit &amp; loss, and balance sheets);&lt;/li&gt;
&lt;li&gt;Learn how to actually apply this to something tangible (real life assets).&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;This post represents some of my learnings and attempts to apply them to a game that features a number of usefully illustrative financial transaction examples, as we’ll see later.&lt;/p&gt;
&lt;p&gt;To reiterate - I am not a bookkeeper, so my explanations are simply the way I describe the key concepts to myself. I acknowledge that my descriptions may not be 100% technically accurate from an accounting perspective, but I hope they will provide a decent understanding to others also starting out. This is still a learning exercise for me, so if anyone has any tips or points for improvement it’d be great to hear from you.&lt;/p&gt;
&lt;p&gt;The rest of this post assumes that Ledger is installed. There are &lt;a href=&#34;https://github.com/ledger/ledger&#34; target=&#34;_blank&#34; rel=&#34;noopener noreferrer&#34; &gt;instructions for this on GitHub&lt;/a&gt;.&lt;/p&gt;
&lt;h2 id=&#34;command-line-accounting--bookkeeping-basics&#34;&gt;Command-line accounting &amp; bookkeeping basics&lt;/h2&gt;
&lt;p&gt;I’m not going to go into too much detail here, as there is lots of information on Wikipedia about &lt;a href=&#34;https://en.wikipedia.org/wiki/Bookkeeping&#34; target=&#34;_blank&#34; rel=&#34;noopener noreferrer&#34; &gt;bookkeeping&lt;/a&gt;, &lt;a href=&#34;https://en.wikipedia.org/wiki/Double-entry_bookkeeping&#34; target=&#34;_blank&#34; rel=&#34;noopener noreferrer&#34; &gt;double-entry bookkeeping&lt;/a&gt; and &lt;a href=&#34;https://en.wikipedia.org/wiki/Debits_and_credits&#34; target=&#34;_blank&#34; rel=&#34;noopener noreferrer&#34; &gt;debits and credits&lt;/a&gt;. For me (probably due to my background) the best introduction was actually &lt;a href=&#34;https://www.ledger-cli.org/3.0/doc/ledger3.html#Fat_002dfree-Accounting&#34; target=&#34;_blank&#34; rel=&#34;noopener noreferrer&#34; &gt;Ledger’s own documentation&lt;/a&gt;, which I definitely recommend reading.&lt;/p&gt;
&lt;p&gt;Essentially, in someone’s double-entry world, the core concept is that money (or whatever assets/currencies/commodities you want to journal, but I’ll talk about money here and, later, bells) - like matter and energy - can’t be created or destroyed. In every transaction money must move from one place (account) to another such that the balance of the transaction equals zero, and therefore the balance of all transactions and accounts (books) is also zero (“balancing the books”). To put it another way, each transaction involves debiting one or more accounts and crediting one or more &lt;em&gt;other&lt;/em&gt; accounts, such that the total amount of the credits equals the total amount of the debits. When adding a transaction to a ledger a bookkeeper signifies this by indicating the account(s) that have been debited and the account(s) that have been credited in the transaction.&lt;/p&gt;
&lt;p&gt;In Ledger we do exactly the same by writing the transaction in the plain text file for our ledger:&lt;/p&gt;
&lt;div class=&#34;highlight&#34;&gt;&lt;pre tabindex=&#34;0&#34; style=&#34;background-color:#f0f3f3;-moz-tab-size:2;-o-tab-size:2;tab-size:2;&#34;&gt;&lt;code class=&#34;language-jsx&#34; data-lang=&#34;jsx&#34;&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;&lt;span style=&#34;color:#f60&#34;&gt;2020&lt;/span&gt;&lt;span style=&#34;color:#555&#34;&gt;/&lt;/span&gt;&lt;span style=&#34;color:#f60&#34;&gt;05&lt;/span&gt;&lt;span style=&#34;color:#555&#34;&gt;/&lt;/span&gt;&lt;span style=&#34;color:#f60&#34;&gt;21&lt;/span&gt;  Payee or description
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;  Account1  &lt;span style=&#34;color:#a00;background-color:#faa&#34;&gt;£&lt;/span&gt;&lt;span style=&#34;color:#f60&#34;&gt;50&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;  Account2  &lt;span style=&#34;color:#a00;background-color:#faa&#34;&gt;£&lt;/span&gt;&lt;span style=&#34;color:#555&#34;&gt;-&lt;/span&gt;&lt;span style=&#34;color:#f60&#34;&gt;50&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;In this example, we show that money has been taken from (credited) Account2 and given to (debited) Account1. Note that in bookkeeping a “debit” is when an account receives a benefit and a “credit” is when an account gives a benefit. This is often the opposite to how people understand debit and credit, and there is a good explanation about this &lt;a href=&#34;https://en.wikipedia.org/wiki/Debits_and_credits#Aspects_of_transactions&#34; target=&#34;_blank&#34; rel=&#34;noopener noreferrer&#34; &gt;on Wikipedia&lt;/a&gt;.&lt;/p&gt;
&lt;p&gt;Note that the balance of the transaction is 0 (there is a credit of £50 and a debit of £50).&lt;/p&gt;
&lt;p&gt;In bookkeeping, accounts don’t have to (and usually don’t) refer to just “bank accounts”. Instead, accounts can be thought of as “categories” or “buckets” of money, of which there are four main types (e.g. for a person):&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;Asset (what the person actually owns, such as cash, bank accounts and property)&lt;/li&gt;
&lt;li&gt;Liability (what the person owes to other people/entities)&lt;/li&gt;
&lt;li&gt;Income (where money comes to the person from)&lt;/li&gt;
&lt;li&gt;Expenses (where money going from the person goes)&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;There are also “equity” accounts, which indicates the person’s net worth. I won’t talk much about these types of accounts, but when first using Ledger yourself this type of account is what you’ll use to represent your current “net worth” being transferred into your accounted-for assets and liabilities.&lt;/p&gt;
&lt;p&gt;Note also that in Ledger we deal with positive and negative numbers to represent debit and credit respectively. Often bookkeepers don’t use signs to indicate debit/credit and instead rely on the understanding that debit and credit mean different things for different types of accounts. For example, that a debit to an asset account is an increase, whereas a debit to a liability account is a decrease. This can be confusing so I won’t go further into this!&lt;/p&gt;
&lt;p&gt;In Ledger, we can indicate the account category and name to make the ledger more useful for reporting. E.g. to make the previous transaction better:&lt;/p&gt;
&lt;div class=&#34;highlight&#34;&gt;&lt;pre tabindex=&#34;0&#34; style=&#34;background-color:#f0f3f3;-moz-tab-size:2;-o-tab-size:2;tab-size:2;&#34;&gt;&lt;code class=&#34;language-jsx&#34; data-lang=&#34;jsx&#34;&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;&lt;span style=&#34;color:#f60&#34;&gt;2020&lt;/span&gt;&lt;span style=&#34;color:#555&#34;&gt;/&lt;/span&gt;&lt;span style=&#34;color:#f60&#34;&gt;05&lt;/span&gt;&lt;span style=&#34;color:#555&#34;&gt;/&lt;/span&gt;&lt;span style=&#34;color:#f60&#34;&gt;21&lt;/span&gt;  Tesco
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;  Expenses&lt;span style=&#34;color:#555&#34;&gt;:&lt;/span&gt;Groceries   &lt;span style=&#34;color:#a00;background-color:#faa&#34;&gt;£&lt;/span&gt;&lt;span style=&#34;color:#f60&#34;&gt;50&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;  Assets&lt;span style=&#34;color:#555&#34;&gt;:&lt;/span&gt;Bank Account
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;We can leave one value line blank in each transaction and Ledger will automatically balance it when running reports - in this scenario by marking a negative (“credit”) £50 in the final line. This transaction shows account changes in two categories - an expense account and an asset account. When we went to Tesco we used our bank card to buy groceries, which debited our Expenses:Groceries account £50 and credited our Assets:Bank Account account the same amount. The expenses account does not refer to anything we “own”; moreover a debit in an expense represents a loss.&lt;/p&gt;
&lt;p&gt;On the Tesco side, they may have something along these lines:&lt;/p&gt;
&lt;div class=&#34;highlight&#34;&gt;&lt;pre tabindex=&#34;0&#34; style=&#34;background-color:#f0f3f3;-moz-tab-size:2;-o-tab-size:2;tab-size:2;&#34;&gt;&lt;code class=&#34;language-jsx&#34; data-lang=&#34;jsx&#34;&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;&lt;span style=&#34;color:#f60&#34;&gt;2020&lt;/span&gt;&lt;span style=&#34;color:#555&#34;&gt;/&lt;/span&gt;&lt;span style=&#34;color:#f60&#34;&gt;05&lt;/span&gt;&lt;span style=&#34;color:#555&#34;&gt;/&lt;/span&gt;&lt;span style=&#34;color:#f60&#34;&gt;21&lt;/span&gt;  Customer123
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;  Assets&lt;span style=&#34;color:#555&#34;&gt;:&lt;/span&gt;Bank Account     &lt;span style=&#34;color:#a00;background-color:#faa&#34;&gt;£&lt;/span&gt;&lt;span style=&#34;color:#f60&#34;&gt;50&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;  Income&lt;span style=&#34;color:#555&#34;&gt;:&lt;/span&gt;Sales&lt;span style=&#34;color:#555&#34;&gt;:&lt;/span&gt;Groceries  &lt;span style=&#34;color:#a00;background-color:#faa&#34;&gt;£&lt;/span&gt;&lt;span style=&#34;color:#555&#34;&gt;-&lt;/span&gt;&lt;span style=&#34;color:#f60&#34;&gt;50&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;Tesco might associate my card with a unique customer number (&lt;code&gt;Customer123&lt;/code&gt;) and mark the transaction as a credit in their income account and a debit in their bank account - essentially the opposite of my own record. Note that in this example Tesco (a more complex entity than myself) have categorised their income account further by indicating an additional level of precision, allowing them to run reports separately on all income, income from sales, and income specifically from groceries sales.&lt;/p&gt;
&lt;h2 id=&#34;why-animal-crossing&#34;&gt;Why Animal Crossing?&lt;/h2&gt;
&lt;p&gt;I (like many others around the world) have recently become engrossed in &lt;a href=&#34;https://en.wikipedia.org/wiki/Animal_Crossing:_New_Horizons&#34; target=&#34;_blank&#34; rel=&#34;noopener noreferrer&#34; &gt;Animal Crossing: New Horizons&lt;/a&gt; - a video game for the Nintendo Switch. The primary concept of the game is that your character, with the help of &lt;a href=&#34;https://animalcrossing.fandom.com/wiki/Nook_Inc&#34; target=&#34;_blank&#34; rel=&#34;noopener noreferrer&#34; &gt;travel company Nook Inc&lt;/a&gt;, moves to a deserted island, along with a couple of other characters (at first), in order to build a new home and life.&lt;/p&gt;
&lt;p&gt;&lt;img src=&#34;https://wilw.dev/media/blog/acnh1.png&#34; alt=&#34;&#34;&gt;&lt;/p&gt;
&lt;p&gt;When you first arrive at your new island, &lt;a href=&#34;https://animalcrossing.fandom.com/wiki/Tom_Nook&#34; target=&#34;_blank&#34; rel=&#34;noopener noreferrer&#34; &gt;Tom Nook&lt;/a&gt; (owner of Nook Inc) introduces you to the other islanders who have also come along, and to Timmy and Tommy (Tom Nook’s apprentices). Tom, Timmy, Tommy (and other characters who later join) stay with you on your island to help you and your fellow islanders out on your new adventure.&lt;/p&gt;
&lt;p&gt;Throughout the game the player is involved in a number of different types of financial transaction and must take out loans, make sales and purchases in order to progress. As I played, it occurred that this would be a great way to learn and explain basic bookkeeping with Ledger.&lt;/p&gt;
&lt;p&gt;In the game the player works with a number of different asset types. The main currency (money) is &lt;code&gt;Bells&lt;/code&gt; but there is also a loyalty program called &lt;code&gt;Nook Miles&lt;/code&gt; which allows the player to gain points (like air miles or a points card for a store). Luckily Ledger can handle different currencies and commodities with no problem - and will even make conversions where it can too - so we can deal with all of our Animal Crossing assets in a single ledger.&lt;/p&gt;
&lt;p&gt;The rest of this post walks through some of the early stages of the game, using it to illustrate basic bookkeeping with Ledger.&lt;/p&gt;
&lt;h2 id=&#34;starting-off-the-ledger&#34;&gt;Starting off the ledger&lt;/h2&gt;
&lt;p&gt;After landing, Tom Nook reveals your accomodation - a tent - and that you have inadvertently already got yourself into debt (from his perspective!). He tells you that the cost of your travel and moving has cost you some Nook Miles loyalty points, which you’ll need to pay back in order to move to the next stages of the game. He does give you the option to pay nearly ten times the amount in Bells instead, but it’s quicker to get the Miles.&lt;/p&gt;
&lt;p&gt;&lt;img src=&#34;https://wilw.dev/media/blog/acnh2.png&#34; alt=&#34;&#34;&gt;&lt;/p&gt;
&lt;p&gt;We now have a financial position (despite it being “negative”). This means that we can add it to our ledger so that we have a clear record of the transaction and can begin to account for all of our finances. Even though you haven’t physically given or received anything yet, you now have a new liability (loan) that has been credited in turn for your moving expense (cost). Inidcate this by creating a new text file (e.g. &lt;code&gt;ledger.dat&lt;/code&gt;) and writing the following:&lt;/p&gt;
&lt;div class=&#34;highlight&#34;&gt;&lt;pre tabindex=&#34;0&#34; style=&#34;background-color:#f0f3f3;-moz-tab-size:2;-o-tab-size:2;tab-size:2;&#34;&gt;&lt;code class=&#34;language-jsx&#34; data-lang=&#34;jsx&#34;&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;&lt;span style=&#34;color:#f60&#34;&gt;2020&lt;/span&gt;&lt;span style=&#34;color:#555&#34;&gt;/&lt;/span&gt;&lt;span style=&#34;color:#f60&#34;&gt;05&lt;/span&gt;&lt;span style=&#34;color:#555&#34;&gt;/&lt;/span&gt;&lt;span style=&#34;color:#f60&#34;&gt;20&lt;/span&gt;  Moving Fee
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;  Expenses&lt;span style=&#34;color:#555&#34;&gt;:&lt;/span&gt;Travel      &lt;span style=&#34;color:#f60&#34;&gt;5000&lt;/span&gt; MILE
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;  Liabilities&lt;span style=&#34;color:#555&#34;&gt;:&lt;/span&gt;NookInc  &lt;span style=&#34;color:#555&#34;&gt;-&lt;/span&gt;&lt;span style=&#34;color:#f60&#34;&gt;5000&lt;/span&gt; MILE
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;Our expenses account has been debited (to show that we have “paid” for something), and our liabilities account has been credited (to indicate that someone else paid on our behalf, and to whom we now owe something). In the real world this would be a similar transaction to when paying for something using a credit card.&lt;/p&gt;
&lt;p&gt;Now we have a transaction entry we can start to run reports. As long as you have Ledger installed, you can run the following to see a register (sort of a transaction statement):&lt;/p&gt;
&lt;div class=&#34;highlight&#34;&gt;&lt;pre tabindex=&#34;0&#34; style=&#34;background-color:#f0f3f3;-moz-tab-size:2;-o-tab-size:2;tab-size:2;&#34;&gt;&lt;code class=&#34;language-jsx&#34; data-lang=&#34;jsx&#34;&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;&lt;span style=&#34;color:#555&#34;&gt;~&lt;/span&gt; ledger &lt;span style=&#34;color:#555&#34;&gt;-&lt;/span&gt;f ledger.dat register
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;&lt;span style=&#34;color:#f60&#34;&gt;20&lt;/span&gt;&lt;span style=&#34;color:#555&#34;&gt;-&lt;/span&gt;May&lt;span style=&#34;color:#555&#34;&gt;-&lt;/span&gt;&lt;span style=&#34;color:#f60&#34;&gt;20&lt;/span&gt; Moving Fee            Expenses&lt;span style=&#34;color:#555&#34;&gt;:&lt;/span&gt;Travel          &lt;span style=&#34;color:#f60&#34;&gt;5000&lt;/span&gt; MILE   &lt;span style=&#34;color:#f60&#34;&gt;5000&lt;/span&gt; MILE
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;                                Liabilities&lt;span style=&#34;color:#555&#34;&gt;:&lt;/span&gt;NookInc     &lt;span style=&#34;color:#555&#34;&gt;-&lt;/span&gt;&lt;span style=&#34;color:#f60&#34;&gt;5000&lt;/span&gt; MILE           &lt;span style=&#34;color:#f60&#34;&gt;0&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;And a current balance (“balance sheet”) of all of our accounts:&lt;/p&gt;
&lt;div class=&#34;highlight&#34;&gt;&lt;pre tabindex=&#34;0&#34; style=&#34;background-color:#f0f3f3;-moz-tab-size:2;-o-tab-size:2;tab-size:2;&#34;&gt;&lt;code class=&#34;language-jsx&#34; data-lang=&#34;jsx&#34;&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;&lt;span style=&#34;color:#555&#34;&gt;~&lt;/span&gt; ledger &lt;span style=&#34;color:#555&#34;&gt;-&lt;/span&gt;f ledger.dat balance
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;          &lt;span style=&#34;color:#f60&#34;&gt;5000&lt;/span&gt; MILE  Expenses&lt;span style=&#34;color:#555&#34;&gt;:&lt;/span&gt;Travel
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;         &lt;span style=&#34;color:#555&#34;&gt;-&lt;/span&gt;&lt;span style=&#34;color:#f60&#34;&gt;5000&lt;/span&gt; MILE  Liabilities&lt;span style=&#34;color:#555&#34;&gt;:&lt;/span&gt;NookInc
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;&lt;span style=&#34;color:#555&#34;&gt;--------------------&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;                   &lt;span style=&#34;color:#f60&#34;&gt;0&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;We can see that Ledger has created the two accounts for us and has checked that they are correctly balanced. At a glance we can easily observe the current state of our finances.&lt;/p&gt;
&lt;p&gt;In future I may sometimes omit the &lt;code&gt;-f ledger.dat&lt;/code&gt; for brevity. You can also do this, for which there are simple instructions on the &lt;a href=&#34;https://wiki.archlinux.org/index.php/Ledger#Usage&#34; target=&#34;_blank&#34; rel=&#34;noopener noreferrer&#34; &gt;Arch Wiki&lt;/a&gt;.&lt;/p&gt;
&lt;h2 id=&#34;paying-off-the-first-loan&#34;&gt;Paying off the first loan&lt;/h2&gt;
&lt;p&gt;Nook Miles can be earned by unlocking achievements in the game (such as picking weeds, fishing, and catching insects). As you unlock an achievement its Nook Miles value will immediately be given to you (i.e. “debited to your assets” - these Miles are now something we “own”). For example, if you were to receive 200 Nook Miles for an achievement then this can be recorded as a new transaction (beneath the first one) in your ledger:&lt;/p&gt;
&lt;div class=&#34;highlight&#34;&gt;&lt;pre tabindex=&#34;0&#34; style=&#34;background-color:#f0f3f3;-moz-tab-size:2;-o-tab-size:2;tab-size:2;&#34;&gt;&lt;code class=&#34;language-jsx&#34; data-lang=&#34;jsx&#34;&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;&lt;span style=&#34;color:#f60&#34;&gt;2020&lt;/span&gt;&lt;span style=&#34;color:#555&#34;&gt;/&lt;/span&gt;&lt;span style=&#34;color:#f60&#34;&gt;05&lt;/span&gt;&lt;span style=&#34;color:#555&#34;&gt;/&lt;/span&gt;&lt;span style=&#34;color:#f60&#34;&gt;20&lt;/span&gt;  Unlocked achievement
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;  Assets&lt;span style=&#34;color:#555&#34;&gt;:&lt;/span&gt;NookMiles     &lt;span style=&#34;color:#f60&#34;&gt;200&lt;/span&gt; MILE
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;  Income&lt;span style=&#34;color:#555&#34;&gt;:&lt;/span&gt;Achievements
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;We can check our balances again to see the new accounts. Note that we don’t need to declare our accounts anywhere; Ledger will automatically create them for us as it finds them.&lt;/p&gt;
&lt;div class=&#34;highlight&#34;&gt;&lt;pre tabindex=&#34;0&#34; style=&#34;background-color:#f0f3f3;-moz-tab-size:2;-o-tab-size:2;tab-size:2;&#34;&gt;&lt;code class=&#34;language-jsx&#34; data-lang=&#34;jsx&#34;&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;&lt;span style=&#34;color:#555&#34;&gt;~&lt;/span&gt; ledger bal
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;            &lt;span style=&#34;color:#f60&#34;&gt;200&lt;/span&gt; MILE  Assets&lt;span style=&#34;color:#555&#34;&gt;:&lt;/span&gt;NookMiles
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;           &lt;span style=&#34;color:#f60&#34;&gt;5000&lt;/span&gt; MILE  Expenses&lt;span style=&#34;color:#555&#34;&gt;:&lt;/span&gt;Travel
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;           &lt;span style=&#34;color:#555&#34;&gt;-&lt;/span&gt;&lt;span style=&#34;color:#f60&#34;&gt;200&lt;/span&gt; MILE  Income&lt;span style=&#34;color:#555&#34;&gt;:&lt;/span&gt;Achievements
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;          &lt;span style=&#34;color:#555&#34;&gt;-&lt;/span&gt;&lt;span style=&#34;color:#f60&#34;&gt;5000&lt;/span&gt; MILE  Liabilities&lt;span style=&#34;color:#555&#34;&gt;:&lt;/span&gt;NookInc
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;&lt;span style=&#34;color:#555&#34;&gt;--------------------&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;                   &lt;span style=&#34;color:#f60&#34;&gt;0&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;We can now also use the &lt;code&gt;balance&lt;/code&gt; report to display our current profit and loss statement (P&amp;L) by limiting its response to just our income and expense accounts:&lt;/p&gt;
&lt;div class=&#34;highlight&#34;&gt;&lt;pre tabindex=&#34;0&#34; style=&#34;background-color:#f0f3f3;-moz-tab-size:2;-o-tab-size:2;tab-size:2;&#34;&gt;&lt;code class=&#34;language-jsx&#34; data-lang=&#34;jsx&#34;&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;&lt;span style=&#34;color:#555&#34;&gt;~&lt;/span&gt; ledger bal Income Expense
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;           &lt;span style=&#34;color:#f60&#34;&gt;5000&lt;/span&gt; MILE  Expenses&lt;span style=&#34;color:#555&#34;&gt;:&lt;/span&gt;Travel
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;           &lt;span style=&#34;color:#555&#34;&gt;-&lt;/span&gt;&lt;span style=&#34;color:#f60&#34;&gt;200&lt;/span&gt; MILE  Income&lt;span style=&#34;color:#555&#34;&gt;:&lt;/span&gt;Achievements
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;&lt;span style=&#34;color:#555&#34;&gt;--------------------&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;           &lt;span style=&#34;color:#f60&#34;&gt;4800&lt;/span&gt; MILE
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;Income accounts have a negative balance in Ledger to indicate how you have credited someone else in order to debit another account (e.g. an asset). Expense accounts have positive balance to show that you have debited someone else in order to pay for something or a service. Therefore, this “positive” P&amp;L balance means that we are still loss-making for the report period.&lt;/p&gt;
&lt;p&gt;I have used quotation marks (&#34;) a bit to try to illustrate meaning without upsetting accountants. It’s a complex world and I find that thinking of things - like Ledger does - in positive and negative numbers makes things easier at first.&lt;/p&gt;
&lt;p&gt;Eventually, after playing the game enough, we will have earned enough Miles to pay back our loan to Nook Inc. This state will be represented by additional transactions in the ledger and an eventual balance that shows that our current assets are greater than our liabilities:&lt;/p&gt;
&lt;div class=&#34;highlight&#34;&gt;&lt;pre tabindex=&#34;0&#34; style=&#34;background-color:#f0f3f3;-moz-tab-size:2;-o-tab-size:2;tab-size:2;&#34;&gt;&lt;code class=&#34;language-jsx&#34; data-lang=&#34;jsx&#34;&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;&lt;span style=&#34;color:#555&#34;&gt;~&lt;/span&gt; ledger bal Assets Liabilities
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;           &lt;span style=&#34;color:#f60&#34;&gt;6200&lt;/span&gt; MILE  Assets&lt;span style=&#34;color:#555&#34;&gt;:&lt;/span&gt;NookMiles
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;          &lt;span style=&#34;color:#555&#34;&gt;-&lt;/span&gt;&lt;span style=&#34;color:#f60&#34;&gt;5000&lt;/span&gt; MILE  Liabilities&lt;span style=&#34;color:#555&#34;&gt;:&lt;/span&gt;NookInc
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;&lt;span style=&#34;color:#555&#34;&gt;--------------------&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;           &lt;span style=&#34;color:#f60&#34;&gt;1200&lt;/span&gt; MILE
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;and the associated P&amp;L to show that we’re now in profit by the same amount:&lt;/p&gt;
&lt;div class=&#34;highlight&#34;&gt;&lt;pre tabindex=&#34;0&#34; style=&#34;background-color:#f0f3f3;-moz-tab-size:2;-o-tab-size:2;tab-size:2;&#34;&gt;&lt;code class=&#34;language-jsx&#34; data-lang=&#34;jsx&#34;&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;&lt;span style=&#34;color:#555&#34;&gt;~&lt;/span&gt; ledger bal Income Expense
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;           &lt;span style=&#34;color:#f60&#34;&gt;5000&lt;/span&gt; MILE  Expenses&lt;span style=&#34;color:#555&#34;&gt;:&lt;/span&gt;Travel
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;          &lt;span style=&#34;color:#555&#34;&gt;-&lt;/span&gt;&lt;span style=&#34;color:#f60&#34;&gt;6200&lt;/span&gt; MILE  Income&lt;span style=&#34;color:#555&#34;&gt;:&lt;/span&gt;Achievements
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;&lt;span style=&#34;color:#555&#34;&gt;--------------------&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;          &lt;span style=&#34;color:#555&#34;&gt;-&lt;/span&gt;&lt;span style=&#34;color:#f60&#34;&gt;1200&lt;/span&gt; MILE
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;When we visit Resident Services to pay back our Miles we log the transaction beneath the first two in the ledger file:&lt;/p&gt;
&lt;div class=&#34;highlight&#34;&gt;&lt;pre tabindex=&#34;0&#34; style=&#34;background-color:#f0f3f3;-moz-tab-size:2;-o-tab-size:2;tab-size:2;&#34;&gt;&lt;code class=&#34;language-jsx&#34; data-lang=&#34;jsx&#34;&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;&lt;span style=&#34;color:#f60&#34;&gt;2020&lt;/span&gt;&lt;span style=&#34;color:#555&#34;&gt;/&lt;/span&gt;&lt;span style=&#34;color:#f60&#34;&gt;05&lt;/span&gt;&lt;span style=&#34;color:#555&#34;&gt;/&lt;/span&gt;&lt;span style=&#34;color:#f60&#34;&gt;21&lt;/span&gt;  Moving fee repayment
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;  Liabilities&lt;span style=&#34;color:#555&#34;&gt;:&lt;/span&gt;NookInc  &lt;span style=&#34;color:#f60&#34;&gt;5000&lt;/span&gt; MILE
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;  Assets&lt;span style=&#34;color:#555&#34;&gt;:&lt;/span&gt;NookMiles
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;And can now show that we now have no more liabilities:&lt;/p&gt;
&lt;div class=&#34;highlight&#34;&gt;&lt;pre tabindex=&#34;0&#34; style=&#34;background-color:#f0f3f3;-moz-tab-size:2;-o-tab-size:2;tab-size:2;&#34;&gt;&lt;code class=&#34;language-jsx&#34; data-lang=&#34;jsx&#34;&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;&lt;span style=&#34;color:#555&#34;&gt;~&lt;/span&gt; ledger bal Assets Liabilities
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;           &lt;span style=&#34;color:#f60&#34;&gt;1200&lt;/span&gt; MILE  Assets&lt;span style=&#34;color:#555&#34;&gt;:&lt;/span&gt;NookMiles
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;For interest, we can view a register (&lt;code&gt;reg&lt;/code&gt;) of all transactions on our assets accounts to date, where the final value is the balance indicated above:&lt;/p&gt;
&lt;div class=&#34;highlight&#34;&gt;&lt;pre tabindex=&#34;0&#34; style=&#34;background-color:#f0f3f3;-moz-tab-size:2;-o-tab-size:2;tab-size:2;&#34;&gt;&lt;code class=&#34;language-jsx&#34; data-lang=&#34;jsx&#34;&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;&lt;span style=&#34;color:#555&#34;&gt;~&lt;/span&gt; ledger reg Assets
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;&lt;span style=&#34;color:#f60&#34;&gt;20&lt;/span&gt;&lt;span style=&#34;color:#555&#34;&gt;-&lt;/span&gt;May&lt;span style=&#34;color:#555&#34;&gt;-&lt;/span&gt;&lt;span style=&#34;color:#f60&#34;&gt;20&lt;/span&gt; Unlocked achievement  Assets&lt;span style=&#34;color:#555&#34;&gt;:&lt;/span&gt;NookMiles           &lt;span style=&#34;color:#f60&#34;&gt;200&lt;/span&gt; MILE     &lt;span style=&#34;color:#f60&#34;&gt;200&lt;/span&gt; MILE
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;&lt;span style=&#34;color:#f60&#34;&gt;20&lt;/span&gt;&lt;span style=&#34;color:#555&#34;&gt;-&lt;/span&gt;May&lt;span style=&#34;color:#555&#34;&gt;-&lt;/span&gt;&lt;span style=&#34;color:#f60&#34;&gt;21&lt;/span&gt; Unlocked acheivements Assets&lt;span style=&#34;color:#555&#34;&gt;:&lt;/span&gt;NookMiles          &lt;span style=&#34;color:#f60&#34;&gt;6000&lt;/span&gt; MILE    &lt;span style=&#34;color:#f60&#34;&gt;6200&lt;/span&gt; MILE
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;&lt;span style=&#34;color:#f60&#34;&gt;20&lt;/span&gt;&lt;span style=&#34;color:#555&#34;&gt;-&lt;/span&gt;May&lt;span style=&#34;color:#555&#34;&gt;-&lt;/span&gt;&lt;span style=&#34;color:#f60&#34;&gt;21&lt;/span&gt; Moving fee repayment  Assets&lt;span style=&#34;color:#555&#34;&gt;:&lt;/span&gt;NookMiles         &lt;span style=&#34;color:#555&#34;&gt;-&lt;/span&gt;&lt;span style=&#34;color:#f60&#34;&gt;5000&lt;/span&gt; MILE    &lt;span style=&#34;color:#f60&#34;&gt;1200&lt;/span&gt; MILE
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;Depending on your play style or needed accuracy you may want to just record achievements (and anything else) in bulk in a single transaction (as in the second posting above) rather than as individual transactions.&lt;/p&gt;
&lt;p&gt;From now on, this post assumes that new transactions continue to be entered beneath each other in your ledger text file.&lt;/p&gt;
&lt;h2 id=&#34;multiple-currency-and-commodity-types&#34;&gt;Multiple currency and commodity types&lt;/h2&gt;
&lt;p&gt;As you play more you’ll quickly realise that you spend a lot of game time with liabilities, which should be a familiar concept to those who have real-life mortgages and credit cards. As soon as you’ve finished paying off your moving fee you’ll be invited to take out a new loan (this time in Bells - the main currency) to upgrade your tent to a house, which you can represent as expected:&lt;/p&gt;
&lt;div class=&#34;highlight&#34;&gt;&lt;pre tabindex=&#34;0&#34; style=&#34;background-color:#f0f3f3;-moz-tab-size:2;-o-tab-size:2;tab-size:2;&#34;&gt;&lt;code class=&#34;language-jsx&#34; data-lang=&#34;jsx&#34;&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;&lt;span style=&#34;color:#f60&#34;&gt;2020&lt;/span&gt;&lt;span style=&#34;color:#555&#34;&gt;/&lt;/span&gt;&lt;span style=&#34;color:#f60&#34;&gt;05&lt;/span&gt;&lt;span style=&#34;color:#555&#34;&gt;/&lt;/span&gt;&lt;span style=&#34;color:#f60&#34;&gt;23&lt;/span&gt;  House upgrade
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;  Assets&lt;span style=&#34;color:#555&#34;&gt;:&lt;/span&gt;Property      &lt;span style=&#34;color:#f60&#34;&gt;98&lt;/span&gt;,&lt;span style=&#34;color:#f60&#34;&gt;000&lt;/span&gt; BELL
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;  Liabilities&lt;span style=&#34;color:#555&#34;&gt;:&lt;/span&gt;NookInc
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;Running a balance sheet report now shows your assets across both Bells and Miles:&lt;/p&gt;
&lt;div class=&#34;highlight&#34;&gt;&lt;pre tabindex=&#34;0&#34; style=&#34;background-color:#f0f3f3;-moz-tab-size:2;-o-tab-size:2;tab-size:2;&#34;&gt;&lt;code class=&#34;language-jsx&#34; data-lang=&#34;jsx&#34;&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;&lt;span style=&#34;color:#555&#34;&gt;~&lt;/span&gt; ledger &lt;span style=&#34;color:#555&#34;&gt;-&lt;/span&gt;f bal
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;         &lt;span style=&#34;color:#f60&#34;&gt;98&lt;/span&gt;,&lt;span style=&#34;color:#f60&#34;&gt;000&lt;/span&gt; BELL
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;           &lt;span style=&#34;color:#f60&#34;&gt;1200&lt;/span&gt; MILE  Assets
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;           &lt;span style=&#34;color:#f60&#34;&gt;1200&lt;/span&gt; MILE    NookMiles
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;         &lt;span style=&#34;color:#f60&#34;&gt;98&lt;/span&gt;,&lt;span style=&#34;color:#f60&#34;&gt;000&lt;/span&gt; BELL    Property
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;           &lt;span style=&#34;color:#f60&#34;&gt;5000&lt;/span&gt; MILE  Expenses&lt;span style=&#34;color:#555&#34;&gt;:&lt;/span&gt;Travel
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;          &lt;span style=&#34;color:#555&#34;&gt;-&lt;/span&gt;&lt;span style=&#34;color:#f60&#34;&gt;6200&lt;/span&gt; MILE  Income&lt;span style=&#34;color:#555&#34;&gt;:&lt;/span&gt;Achievements
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;        &lt;span style=&#34;color:#555&#34;&gt;-&lt;/span&gt;&lt;span style=&#34;color:#f60&#34;&gt;98&lt;/span&gt;,&lt;span style=&#34;color:#f60&#34;&gt;000&lt;/span&gt; BELL  Liabilities&lt;span style=&#34;color:#555&#34;&gt;:&lt;/span&gt;NookInc
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;&lt;span style=&#34;color:#555&#34;&gt;--------------------&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;                   &lt;span style=&#34;color:#f60&#34;&gt;0&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;From your perspective, Bells are “currency” and Miles are “commodities”. From Ledger’s view they’re just two types of “things” that you can account for, and doesn’t differentiate between them. Note also that Ledger has learned how we want things displayed based on our early transactions: the value always comes before the commodity symbol, and Bells are displayed with commas to separate every third digit whereas Miles are displayed uninterrupted.&lt;/p&gt;
&lt;p&gt;Although your new house is an asset (subclassed as a property), it also has the associated liability (loan) that you’ll gradually pay off - this time through your ABD (as we’ll see later) - as you did with the Nook Miles. As such, your net worth (assets minus liabilities) is still currently only the leftover Miles you have from your first loan:&lt;/p&gt;
&lt;div class=&#34;highlight&#34;&gt;&lt;pre tabindex=&#34;0&#34; style=&#34;background-color:#f0f3f3;-moz-tab-size:2;-o-tab-size:2;tab-size:2;&#34;&gt;&lt;code class=&#34;language-jsx&#34; data-lang=&#34;jsx&#34;&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;&lt;span style=&#34;color:#555&#34;&gt;~&lt;/span&gt; ledger bal Assets Liabilities
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;         &lt;span style=&#34;color:#f60&#34;&gt;98&lt;/span&gt;,&lt;span style=&#34;color:#f60&#34;&gt;000&lt;/span&gt; BELL
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;           &lt;span style=&#34;color:#f60&#34;&gt;1200&lt;/span&gt; MILE  Assets
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;           &lt;span style=&#34;color:#f60&#34;&gt;1200&lt;/span&gt; MILE    NookMiles
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;         &lt;span style=&#34;color:#f60&#34;&gt;98&lt;/span&gt;,&lt;span style=&#34;color:#f60&#34;&gt;000&lt;/span&gt; BELL    Property
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;        &lt;span style=&#34;color:#555&#34;&gt;-&lt;/span&gt;&lt;span style=&#34;color:#f60&#34;&gt;98&lt;/span&gt;,&lt;span style=&#34;color:#f60&#34;&gt;000&lt;/span&gt; BELL  Liabilities&lt;span style=&#34;color:#555&#34;&gt;:&lt;/span&gt;NookInc
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;&lt;span style=&#34;color:#555&#34;&gt;--------------------&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;           &lt;span style=&#34;color:#f60&#34;&gt;1200&lt;/span&gt; MILE
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;h2 id=&#34;income-and-expenses&#34;&gt;Income and expenses&lt;/h2&gt;
&lt;p&gt;At the start of the game, Timmy will buy items from you (e.g. things you find around the island or are given) in exchange for Bells.&lt;/p&gt;
&lt;p&gt;Selling such items (e.g. fruit you find on the island) is simply represented as a debit in your assets and a credit in your income (which you’ll remember is always “negative”). When you make the sale to Timmy (or in Nook’s Cranny) the Bells are transferred directly to you in person (i.e. “cash”):&lt;/p&gt;
&lt;div class=&#34;highlight&#34;&gt;&lt;pre tabindex=&#34;0&#34; style=&#34;background-color:#f0f3f3;-moz-tab-size:2;-o-tab-size:2;tab-size:2;&#34;&gt;&lt;code class=&#34;language-jsx&#34; data-lang=&#34;jsx&#34;&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;&lt;span style=&#34;color:#f60&#34;&gt;2020&lt;/span&gt;&lt;span style=&#34;color:#555&#34;&gt;/&lt;/span&gt;&lt;span style=&#34;color:#f60&#34;&gt;05&lt;/span&gt;&lt;span style=&#34;color:#555&#34;&gt;/&lt;/span&gt;&lt;span style=&#34;color:#f60&#34;&gt;24&lt;/span&gt;  Timmy
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;  Assets&lt;span style=&#34;color:#555&#34;&gt;:&lt;/span&gt;Cash   &lt;span style=&#34;color:#f60&#34;&gt;8&lt;/span&gt;,&lt;span style=&#34;color:#f60&#34;&gt;000&lt;/span&gt; BELL
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;  Income&lt;span style=&#34;color:#555&#34;&gt;:&lt;/span&gt;Sales
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;When you sell items in this way you increase your net worth, since your assets have increased but your liabilities have not changed.&lt;/p&gt;
&lt;p&gt;&lt;img src=&#34;https://wilw.dev/media/blog/acnh3.png&#34; alt=&#34;&#34;&gt;&lt;/p&gt;
&lt;p&gt;As the game progresses you’ll eventually unlock Nook’s Cranny - Tommy and Timmy’s shop for the island’s residents. Here you can continue to sell things, but you can also now buy items from their rotating stock. When you buy items (e.g. furniture) from the shop, the cash account is instead credited:&lt;/p&gt;
&lt;div class=&#34;highlight&#34;&gt;&lt;pre tabindex=&#34;0&#34; style=&#34;background-color:#f0f3f3;-moz-tab-size:2;-o-tab-size:2;tab-size:2;&#34;&gt;&lt;code class=&#34;language-jsx&#34; data-lang=&#34;jsx&#34;&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;&lt;span style=&#34;color:#f60&#34;&gt;2020&lt;/span&gt;&lt;span style=&#34;color:#555&#34;&gt;/&lt;/span&gt;&lt;span style=&#34;color:#f60&#34;&gt;05&lt;/span&gt;&lt;span style=&#34;color:#555&#34;&gt;/&lt;/span&gt;&lt;span style=&#34;color:#f60&#34;&gt;25&lt;/span&gt;  Nook&lt;span style=&#34;color:#a00;background-color:#faa&#34;&gt;&#39;&lt;/span&gt;s Cranny
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;  Expenses&lt;span style=&#34;color:#555&#34;&gt;:&lt;/span&gt;Furniture   &lt;span style=&#34;color:#f60&#34;&gt;3&lt;/span&gt;,&lt;span style=&#34;color:#f60&#34;&gt;000&lt;/span&gt; BELL
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;  Assets&lt;span style=&#34;color:#555&#34;&gt;:&lt;/span&gt;Cash
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;h2 id=&#34;savings-accounts&#34;&gt;Savings accounts&lt;/h2&gt;
&lt;p&gt;Now you have accumulated some Bells you can pay off your house loans. You’ll be able to continue to take out progressively larger (though thankfully interest-free) loans to upgrade your home a number of times each time you pay one off, until you own a house with multiple rooms, an upstairs and a basement.&lt;/p&gt;
&lt;p&gt;&lt;img src=&#34;https://wilw.dev/media/blog/acnh4.png&#34; alt=&#34;&#34;&gt;&lt;/p&gt;
&lt;p&gt;Paying off your house loans is done through the ABD (Automatic Bell Dispensor) function available on the Nook Stop machine in Resident Services. In the ABD you have something similiar to a savings account, which you can make Bell deposits to and withdrawals from at any time.&lt;/p&gt;
&lt;p&gt;&lt;img src=&#34;https://wilw.dev/media/blog/acnh5.png&#34; alt=&#34;&#34;&gt;&lt;/p&gt;
&lt;p&gt;To pay off your loans you first need to deposit the Bells that you have on you into your savings account. This would be recorded in your ledger as two postings involving asset accounts (a debit in your savings and a credit in your cash balances):&lt;/p&gt;
&lt;div class=&#34;highlight&#34;&gt;&lt;pre tabindex=&#34;0&#34; style=&#34;background-color:#f0f3f3;-moz-tab-size:2;-o-tab-size:2;tab-size:2;&#34;&gt;&lt;code class=&#34;language-jsx&#34; data-lang=&#34;jsx&#34;&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;&lt;span style=&#34;color:#f60&#34;&gt;2020&lt;/span&gt;&lt;span style=&#34;color:#555&#34;&gt;/&lt;/span&gt;&lt;span style=&#34;color:#f60&#34;&gt;05&lt;/span&gt;&lt;span style=&#34;color:#555&#34;&gt;/&lt;/span&gt;&lt;span style=&#34;color:#f60&#34;&gt;26&lt;/span&gt;  Savings transfer
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;  Assets&lt;span style=&#34;color:#555&#34;&gt;:&lt;/span&gt;Savings  &lt;span style=&#34;color:#f60&#34;&gt;4&lt;/span&gt;,&lt;span style=&#34;color:#f60&#34;&gt;000&lt;/span&gt; BELL
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;  Assets&lt;span style=&#34;color:#555&#34;&gt;:&lt;/span&gt;Cash
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;You can now credit the savings balance to pay off some of the loan:&lt;/p&gt;
&lt;div class=&#34;highlight&#34;&gt;&lt;pre tabindex=&#34;0&#34; style=&#34;background-color:#f0f3f3;-moz-tab-size:2;-o-tab-size:2;tab-size:2;&#34;&gt;&lt;code class=&#34;language-jsx&#34; data-lang=&#34;jsx&#34;&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;&lt;span style=&#34;color:#f60&#34;&gt;2020&lt;/span&gt;&lt;span style=&#34;color:#555&#34;&gt;/&lt;/span&gt;&lt;span style=&#34;color:#f60&#34;&gt;05&lt;/span&gt;&lt;span style=&#34;color:#555&#34;&gt;/&lt;/span&gt;&lt;span style=&#34;color:#f60&#34;&gt;26&lt;/span&gt;  Loan repayment
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;  Liabilities&lt;span style=&#34;color:#555&#34;&gt;:&lt;/span&gt;NookInc  &lt;span style=&#34;color:#f60&#34;&gt;4&lt;/span&gt;,&lt;span style=&#34;color:#f60&#34;&gt;000&lt;/span&gt; BELL
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;  Assets&lt;span style=&#34;color:#555&#34;&gt;:&lt;/span&gt;Savings
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;Note that these two transactions have not affected your net worth: even though you have paid off some of the loan, your own assets have also reduced by the same amount. Taking out and repaying loans only “moves” money between assets and liabilities accounts.&lt;/p&gt;
&lt;h2 id=&#34;accounts-receivable&#34;&gt;Accounts receivable&lt;/h2&gt;
&lt;p&gt;Nook’s Cranny closes at 10PM each evening, and purchases and sales that day should ideally be made before that time. However, the shop also offers a “drop-off box” facility that allows you to sell items (at a reduced price) whilst the shop is closed.&lt;/p&gt;
&lt;p&gt;&lt;img src=&#34;https://wilw.dev/media/blog/acnh6.png&#34; alt=&#34;&#34;&gt;&lt;/p&gt;
&lt;p&gt;Sales made this way do not transfer cash directly to you, as they do when you sell directly when the shop is open; instead the Bells accumulated by selling to the drop-off box are transferred to your savings account when the shop opens the &lt;em&gt;following day&lt;/em&gt;.&lt;/p&gt;
&lt;p&gt;As such, when using this service, although you have sold items, the Bells are not tangibly (physically) yours to spend straight away. They are still assets (i.e. they increase your net worth), but they should be recorded separately to your cash and savings accounts by using an “accounts receivable” assets account - which represents money you are &lt;em&gt;owed&lt;/em&gt;:&lt;/p&gt;
&lt;div class=&#34;highlight&#34;&gt;&lt;pre tabindex=&#34;0&#34; style=&#34;background-color:#f0f3f3;-moz-tab-size:2;-o-tab-size:2;tab-size:2;&#34;&gt;&lt;code class=&#34;language-jsx&#34; data-lang=&#34;jsx&#34;&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;&lt;span style=&#34;color:#f60&#34;&gt;2020&lt;/span&gt;&lt;span style=&#34;color:#555&#34;&gt;/&lt;/span&gt;&lt;span style=&#34;color:#f60&#34;&gt;05&lt;/span&gt;&lt;span style=&#34;color:#555&#34;&gt;/&lt;/span&gt;&lt;span style=&#34;color:#f60&#34;&gt;27&lt;/span&gt;  Nook&lt;span style=&#34;color:#a00;background-color:#faa&#34;&gt;&#39;&lt;/span&gt;s Cranny
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;  Assets&lt;span style=&#34;color:#555&#34;&gt;:&lt;/span&gt;Accounts Receivable  &lt;span style=&#34;color:#f60&#34;&gt;8&lt;/span&gt;,&lt;span style=&#34;color:#f60&#34;&gt;000&lt;/span&gt; BELL
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;  Income&lt;span style=&#34;color:#555&#34;&gt;:&lt;/span&gt;Sales
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;If you run a business that makes sales through invoices, checking a receivables account is a nice way to view money that (hopefully) will be paid to you soon:&lt;/p&gt;
&lt;div class=&#34;highlight&#34;&gt;&lt;pre tabindex=&#34;0&#34; style=&#34;background-color:#f0f3f3;-moz-tab-size:2;-o-tab-size:2;tab-size:2;&#34;&gt;&lt;code class=&#34;language-jsx&#34; data-lang=&#34;jsx&#34;&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;&lt;span style=&#34;color:#555&#34;&gt;~&lt;/span&gt; ledger reg Receivable
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;&lt;span style=&#34;color:#f60&#34;&gt;20&lt;/span&gt;&lt;span style=&#34;color:#555&#34;&gt;-&lt;/span&gt;May&lt;span style=&#34;color:#555&#34;&gt;-&lt;/span&gt;&lt;span style=&#34;color:#f60&#34;&gt;27&lt;/span&gt; Nook&lt;span style=&#34;color:#a00;background-color:#faa&#34;&gt;&#39;&lt;/span&gt;s Cranny    Assets&lt;span style=&#34;color:#555&#34;&gt;:&lt;/span&gt;Accounts Receivable   &lt;span style=&#34;color:#f60&#34;&gt;8&lt;/span&gt;,&lt;span style=&#34;color:#f60&#34;&gt;000&lt;/span&gt; BELL   &lt;span style=&#34;color:#f60&#34;&gt;8&lt;/span&gt;,&lt;span style=&#34;color:#f60&#34;&gt;000&lt;/span&gt; BELL
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;The following day, when the shop re-opens, you get a call to let you know that you have now received the funds to your savings account, which you add to your ledger:&lt;/p&gt;
&lt;div class=&#34;highlight&#34;&gt;&lt;pre tabindex=&#34;0&#34; style=&#34;background-color:#f0f3f3;-moz-tab-size:2;-o-tab-size:2;tab-size:2;&#34;&gt;&lt;code class=&#34;language-jsx&#34; data-lang=&#34;jsx&#34;&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;&lt;span style=&#34;color:#f60&#34;&gt;2020&lt;/span&gt;&lt;span style=&#34;color:#555&#34;&gt;/&lt;/span&gt;&lt;span style=&#34;color:#f60&#34;&gt;05&lt;/span&gt;&lt;span style=&#34;color:#555&#34;&gt;/&lt;/span&gt;&lt;span style=&#34;color:#f60&#34;&gt;28&lt;/span&gt;  Nook&lt;span style=&#34;color:#a00;background-color:#faa&#34;&gt;&#39;&lt;/span&gt;s Cranny
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;  Assets&lt;span style=&#34;color:#555&#34;&gt;:&lt;/span&gt;Savings               &lt;span style=&#34;color:#f60&#34;&gt;8&lt;/span&gt;,&lt;span style=&#34;color:#f60&#34;&gt;000&lt;/span&gt; BELL
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;  Assets&lt;span style=&#34;color:#555&#34;&gt;:&lt;/span&gt;Accounts Receivable
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;Ledger actually has nicer ways of managing this for you (e.g. &lt;a href=&#34;https://www.ledger-cli.org/3.0/doc/ledger3.html#Effective-Dates&#34; target=&#34;_blank&#34; rel=&#34;noopener noreferrer&#34; &gt;effective dates&lt;/a&gt;), but this is a simple example to illustrate receivable accounts.&lt;/p&gt;
&lt;p&gt;Accounts payable are the opposite; they are classed as liabilities since this is money you &lt;em&gt;owe&lt;/em&gt;. In many cases you wouldn’t normally have a generic “payable” account - instead opting to use specific payables for individual liabilities (e.g. a particular credit card or mortgage).&lt;/p&gt;
&lt;h2 id=&#34;joint-accounts-payees-and-tags&#34;&gt;Joint accounts, payees and tags&lt;/h2&gt;
&lt;p&gt;Your island will contain a number of streams and cliffs that need to be traversed using tools that you can make yourself. However, Tom Nook also gives the option to purchase bridges and inclines to allow you and fellow island residents to more easily get around.&lt;/p&gt;
&lt;p&gt;Once the plan for the building work (e.g. a bridge) is laid-down, &lt;a href=&#34;https://animalcrossing.fandom.com/wiki/Lloid&#34; target=&#34;_blank&#34; rel=&#34;noopener noreferrer&#34; &gt;Lloid&lt;/a&gt; collects funds for the work before it can begin. Funds can be contributed by yourself, NPC islanders, or by other players who visit your island. Like me, you’ll likely find that your islander neighbours are less than generous when it comes to infrastructure investment, however it is interesting to consider this concept as a joint account shared by multiple people or entities.&lt;/p&gt;
&lt;p&gt;From my perspective, there is probably no right or wrong way to bookkeep this. The constructed bridge is not really an asset that you own (since it is a public object), however you’ll certainly need to contribute towards it if you want it built any time soon. As such, you may want to simply consider it as an expense:&lt;/p&gt;
&lt;div class=&#34;highlight&#34;&gt;&lt;pre tabindex=&#34;0&#34; style=&#34;background-color:#f0f3f3;-moz-tab-size:2;-o-tab-size:2;tab-size:2;&#34;&gt;&lt;code class=&#34;language-jsx&#34; data-lang=&#34;jsx&#34;&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;&lt;span style=&#34;color:#f60&#34;&gt;2020&lt;/span&gt;&lt;span style=&#34;color:#555&#34;&gt;/&lt;/span&gt;&lt;span style=&#34;color:#f60&#34;&gt;06&lt;/span&gt;&lt;span style=&#34;color:#555&#34;&gt;/&lt;/span&gt;&lt;span style=&#34;color:#f60&#34;&gt;01&lt;/span&gt;  Lloid
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;  ; &lt;span style=&#34;color:#555&#34;&gt;:&lt;/span&gt;bridge1&lt;span style=&#34;color:#555&#34;&gt;:&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;  Expenses&lt;span style=&#34;color:#555&#34;&gt;:&lt;/span&gt;Infrastructure   &lt;span style=&#34;color:#f60&#34;&gt;20&lt;/span&gt;,&lt;span style=&#34;color:#f60&#34;&gt;000&lt;/span&gt; BELL
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;  Assets&lt;span style=&#34;color:#555&#34;&gt;:&lt;/span&gt;Cash
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;The commented &lt;code&gt;:bridge1:&lt;/code&gt; is a tag for the transaction (&lt;a href=&#34;https://www.ledger-cli.org/3.0/doc/ledger3.html#Metadata-tags&#34; target=&#34;_blank&#34; rel=&#34;noopener noreferrer&#34; &gt;read more&lt;/a&gt; about how Ledger can handle tags). If I later made further payments towards this bridge I could re-use the same tag and then, when runing reports later, use Ledger to view all of my payments towards this particular bridge:&lt;/p&gt;
&lt;div class=&#34;highlight&#34;&gt;&lt;pre tabindex=&#34;0&#34; style=&#34;background-color:#f0f3f3;-moz-tab-size:2;-o-tab-size:2;tab-size:2;&#34;&gt;&lt;code class=&#34;language-jsx&#34; data-lang=&#34;jsx&#34;&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;&lt;span style=&#34;color:#555&#34;&gt;~&lt;/span&gt; ledger reg &lt;span style=&#34;color:#555&#34;&gt;%&lt;/span&gt;bridge1
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;&lt;span style=&#34;color:#f60&#34;&gt;20&lt;/span&gt;&lt;span style=&#34;color:#555&#34;&gt;-&lt;/span&gt;May&lt;span style=&#34;color:#555&#34;&gt;-&lt;/span&gt;&lt;span style=&#34;color:#f60&#34;&gt;30&lt;/span&gt; Lloid                 Expense&lt;span style=&#34;color:#555&#34;&gt;:&lt;/span&gt;Infrastructure  &lt;span style=&#34;color:#f60&#34;&gt;20&lt;/span&gt;,&lt;span style=&#34;color:#f60&#34;&gt;000&lt;/span&gt; BELL  &lt;span style=&#34;color:#f60&#34;&gt;20&lt;/span&gt;,&lt;span style=&#34;color:#f60&#34;&gt;000&lt;/span&gt; BELL
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;                                Assets&lt;span style=&#34;color:#555&#34;&gt;:&lt;/span&gt;Cash            &lt;span style=&#34;color:#555&#34;&gt;-&lt;/span&gt;&lt;span style=&#34;color:#f60&#34;&gt;20&lt;/span&gt;,&lt;span style=&#34;color:#f60&#34;&gt;000&lt;/span&gt; BELL            &lt;span style=&#34;color:#f60&#34;&gt;0&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;&lt;span style=&#34;color:#f60&#34;&gt;20&lt;/span&gt;&lt;span style=&#34;color:#555&#34;&gt;-&lt;/span&gt;Jun&lt;span style=&#34;color:#555&#34;&gt;-&lt;/span&gt;&lt;span style=&#34;color:#f60&#34;&gt;01&lt;/span&gt; Lloid                 Expense&lt;span style=&#34;color:#555&#34;&gt;:&lt;/span&gt;Infrastructure   &lt;span style=&#34;color:#f60&#34;&gt;5&lt;/span&gt;,&lt;span style=&#34;color:#f60&#34;&gt;000&lt;/span&gt; BELL   &lt;span style=&#34;color:#f60&#34;&gt;5&lt;/span&gt;,&lt;span style=&#34;color:#f60&#34;&gt;000&lt;/span&gt; BELL
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;                                Assets&lt;span style=&#34;color:#555&#34;&gt;:&lt;/span&gt;Cash
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;I think it’d be tricky to account for this yourself in other ways, since you do not know who has contributed what.  As such your ledger would not be aware of the total cost of the bridge before it has been built.&lt;/p&gt;
&lt;p&gt;&lt;img src=&#34;https://wilw.dev/media/blog/acnh7.png&#34; alt=&#34;&#34;&gt;&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Considering the process from Lloid’s perspective&lt;/strong&gt;, my first contribution from above could be marked as the following in his own ledger:&lt;/p&gt;
&lt;div class=&#34;highlight&#34;&gt;&lt;pre tabindex=&#34;0&#34; style=&#34;background-color:#f0f3f3;-moz-tab-size:2;-o-tab-size:2;tab-size:2;&#34;&gt;&lt;code class=&#34;language-jsx&#34; data-lang=&#34;jsx&#34;&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;&lt;span style=&#34;color:#f60&#34;&gt;2020&lt;/span&gt;&lt;span style=&#34;color:#555&#34;&gt;/&lt;/span&gt;&lt;span style=&#34;color:#f60&#34;&gt;06&lt;/span&gt;&lt;span style=&#34;color:#555&#34;&gt;/&lt;/span&gt;&lt;span style=&#34;color:#f60&#34;&gt;01&lt;/span&gt;  Bridge payment
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;  ; &lt;span style=&#34;color:#555&#34;&gt;:&lt;/span&gt;infrastructure&lt;span style=&#34;color:#555&#34;&gt;:&lt;/span&gt;southBridge&lt;span style=&#34;color:#555&#34;&gt;:&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;  ; Payee&lt;span style=&#34;color:#555&#34;&gt;:&lt;/span&gt; Will
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;  Assets&lt;span style=&#34;color:#555&#34;&gt;:&lt;/span&gt;Cash             &lt;span style=&#34;color:#f60&#34;&gt;20&lt;/span&gt;,&lt;span style=&#34;color:#f60&#34;&gt;000&lt;/span&gt; BELL
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;  Income&lt;span style=&#34;color:#555&#34;&gt;:&lt;/span&gt;Contributions
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;Here Lloid has also tagged the transaction (though he refers to the bridge differently) and also adds a special &lt;a href=&#34;https://www.ledger-cli.org/3.0/doc/ledger3.html#Payee-metadata-tag&#34; target=&#34;_blank&#34; rel=&#34;noopener noreferrer&#34; &gt;&lt;code&gt;Payee&lt;/code&gt; tag&lt;/a&gt; to the transaction. When more people have contributed he can clearly report on who has contributed what for this particular bridge:&lt;/p&gt;
&lt;div class=&#34;highlight&#34;&gt;&lt;pre tabindex=&#34;0&#34; style=&#34;background-color:#f0f3f3;-moz-tab-size:2;-o-tab-size:2;tab-size:2;&#34;&gt;&lt;code class=&#34;language-jsx&#34; data-lang=&#34;jsx&#34;&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;&lt;span style=&#34;color:#555&#34;&gt;~&lt;/span&gt; ledger reg &lt;span style=&#34;color:#555&#34;&gt;%&lt;/span&gt;southBridge &lt;span style=&#34;color:#555&#34;&gt;--&lt;/span&gt;by&lt;span style=&#34;color:#555&#34;&gt;-&lt;/span&gt;payee
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;&lt;span style=&#34;color:#f60&#34;&gt;20&lt;/span&gt;&lt;span style=&#34;color:#555&#34;&gt;-&lt;/span&gt;Jun&lt;span style=&#34;color:#555&#34;&gt;-&lt;/span&gt;&lt;span style=&#34;color:#f60&#34;&gt;02&lt;/span&gt; Cleo                  Assets&lt;span style=&#34;color:#555&#34;&gt;:&lt;/span&gt;Cash             &lt;span style=&#34;color:#f60&#34;&gt;10&lt;/span&gt;,&lt;span style=&#34;color:#f60&#34;&gt;000&lt;/span&gt; BELL  &lt;span style=&#34;color:#f60&#34;&gt;10&lt;/span&gt;,&lt;span style=&#34;color:#f60&#34;&gt;000&lt;/span&gt; BELL
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;                                Income&lt;span style=&#34;color:#555&#34;&gt;:&lt;/span&gt;Contributions   &lt;span style=&#34;color:#555&#34;&gt;-&lt;/span&gt;&lt;span style=&#34;color:#f60&#34;&gt;10&lt;/span&gt;,&lt;span style=&#34;color:#f60&#34;&gt;000&lt;/span&gt; BELL            &lt;span style=&#34;color:#f60&#34;&gt;0&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;&lt;span style=&#34;color:#f60&#34;&gt;20&lt;/span&gt;&lt;span style=&#34;color:#555&#34;&gt;-&lt;/span&gt;Jun&lt;span style=&#34;color:#555&#34;&gt;-&lt;/span&gt;&lt;span style=&#34;color:#f60&#34;&gt;03&lt;/span&gt; Punchy                Assets&lt;span style=&#34;color:#555&#34;&gt;:&lt;/span&gt;Cash             &lt;span style=&#34;color:#f60&#34;&gt;15&lt;/span&gt;,&lt;span style=&#34;color:#f60&#34;&gt;000&lt;/span&gt; BELL  &lt;span style=&#34;color:#f60&#34;&gt;15&lt;/span&gt;,&lt;span style=&#34;color:#f60&#34;&gt;000&lt;/span&gt; BELL
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;                                Income&lt;span style=&#34;color:#555&#34;&gt;:&lt;/span&gt;Contributions   &lt;span style=&#34;color:#555&#34;&gt;-&lt;/span&gt;&lt;span style=&#34;color:#f60&#34;&gt;15&lt;/span&gt;,&lt;span style=&#34;color:#f60&#34;&gt;000&lt;/span&gt; BELL            &lt;span style=&#34;color:#f60&#34;&gt;0&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;&lt;span style=&#34;color:#f60&#34;&gt;20&lt;/span&gt;&lt;span style=&#34;color:#555&#34;&gt;-&lt;/span&gt;Jun&lt;span style=&#34;color:#555&#34;&gt;-&lt;/span&gt;&lt;span style=&#34;color:#f60&#34;&gt;01&lt;/span&gt; Will                  Assets&lt;span style=&#34;color:#555&#34;&gt;:&lt;/span&gt;Cash             &lt;span style=&#34;color:#f60&#34;&gt;30&lt;/span&gt;,&lt;span style=&#34;color:#f60&#34;&gt;000&lt;/span&gt; BELL  &lt;span style=&#34;color:#f60&#34;&gt;30&lt;/span&gt;,&lt;span style=&#34;color:#f60&#34;&gt;000&lt;/span&gt; BELL
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;                                Income&lt;span style=&#34;color:#555&#34;&gt;:&lt;/span&gt;Contributions   &lt;span style=&#34;color:#555&#34;&gt;-&lt;/span&gt;&lt;span style=&#34;color:#f60&#34;&gt;30&lt;/span&gt;,&lt;span style=&#34;color:#f60&#34;&gt;000&lt;/span&gt; BELL            &lt;span style=&#34;color:#f60&#34;&gt;0&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;He can also quickly see all infrastructure contributions made by me:&lt;/p&gt;
&lt;div class=&#34;highlight&#34;&gt;&lt;pre tabindex=&#34;0&#34; style=&#34;background-color:#f0f3f3;-moz-tab-size:2;-o-tab-size:2;tab-size:2;&#34;&gt;&lt;code class=&#34;language-jsx&#34; data-lang=&#34;jsx&#34;&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;&lt;span style=&#34;color:#555&#34;&gt;~&lt;/span&gt; ledger bal &lt;span style=&#34;color:#a00;background-color:#faa&#34;&gt;@&lt;/span&gt;Will and &lt;span style=&#34;color:#555&#34;&gt;%&lt;/span&gt;infrastructure
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;         &lt;span style=&#34;color:#f60&#34;&gt;30&lt;/span&gt;,&lt;span style=&#34;color:#f60&#34;&gt;000&lt;/span&gt; BELL  Assets&lt;span style=&#34;color:#555&#34;&gt;:&lt;/span&gt;Cash
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;        &lt;span style=&#34;color:#555&#34;&gt;-&lt;/span&gt;&lt;span style=&#34;color:#f60&#34;&gt;30&lt;/span&gt;,&lt;span style=&#34;color:#f60&#34;&gt;000&lt;/span&gt; BELL  Income&lt;span style=&#34;color:#555&#34;&gt;:&lt;/span&gt;Contributions
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;&lt;span style=&#34;color:#555&#34;&gt;--------------------&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;                   &lt;span style=&#34;color:#f60&#34;&gt;0&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;Note that if we omit the &lt;code&gt;and&lt;/code&gt;, Ledger would read this as “&lt;code&gt;@Will&lt;/code&gt; OR &lt;code&gt;%infrastructure&lt;/code&gt;”, which gives a different balance (i.e. a balance of any transactions involving me OR infrastructure):&lt;/p&gt;
&lt;div class=&#34;highlight&#34;&gt;&lt;pre tabindex=&#34;0&#34; style=&#34;background-color:#f0f3f3;-moz-tab-size:2;-o-tab-size:2;tab-size:2;&#34;&gt;&lt;code class=&#34;language-jsx&#34; data-lang=&#34;jsx&#34;&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;&lt;span style=&#34;color:#555&#34;&gt;~&lt;/span&gt; ledger bal &lt;span style=&#34;color:#a00;background-color:#faa&#34;&gt;@&lt;/span&gt;Will &lt;span style=&#34;color:#555&#34;&gt;%&lt;/span&gt;infrastructure
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;         &lt;span style=&#34;color:#f60&#34;&gt;55&lt;/span&gt;,&lt;span style=&#34;color:#f60&#34;&gt;000&lt;/span&gt; BELL  Assets&lt;span style=&#34;color:#555&#34;&gt;:&lt;/span&gt;Cash
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;        &lt;span style=&#34;color:#555&#34;&gt;-&lt;/span&gt;&lt;span style=&#34;color:#f60&#34;&gt;55&lt;/span&gt;,&lt;span style=&#34;color:#f60&#34;&gt;000&lt;/span&gt; BELL  Income&lt;span style=&#34;color:#555&#34;&gt;:&lt;/span&gt;Contributions
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;&lt;span style=&#34;color:#555&#34;&gt;--------------------&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;                   &lt;span style=&#34;color:#f60&#34;&gt;0&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;Later, Lloid would use his contributed assets to pay for the materials and labour required to build the bridge (keeping some in his savings account as profit, of course):&lt;/p&gt;
&lt;div class=&#34;highlight&#34;&gt;&lt;pre tabindex=&#34;0&#34; style=&#34;background-color:#f0f3f3;-moz-tab-size:2;-o-tab-size:2;tab-size:2;&#34;&gt;&lt;code class=&#34;language-jsx&#34; data-lang=&#34;jsx&#34;&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;&lt;span style=&#34;color:#f60&#34;&gt;2020&lt;/span&gt;&lt;span style=&#34;color:#555&#34;&gt;/&lt;/span&gt;&lt;span style=&#34;color:#f60&#34;&gt;06&lt;/span&gt;&lt;span style=&#34;color:#555&#34;&gt;/&lt;/span&gt;&lt;span style=&#34;color:#f60&#34;&gt;05&lt;/span&gt;  Bridge build
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;  ; &lt;span style=&#34;color:#555&#34;&gt;:&lt;/span&gt;infrastructure&lt;span style=&#34;color:#555&#34;&gt;:&lt;/span&gt;southBridge&lt;span style=&#34;color:#555&#34;&gt;:&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;  Expenses&lt;span style=&#34;color:#555&#34;&gt;:&lt;/span&gt;Materials             &lt;span style=&#34;color:#f60&#34;&gt;60&lt;/span&gt;,&lt;span style=&#34;color:#f60&#34;&gt;000&lt;/span&gt; BELL
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;  Expenses&lt;span style=&#34;color:#555&#34;&gt;:&lt;/span&gt;Labour                &lt;span style=&#34;color:#f60&#34;&gt;40&lt;/span&gt;,&lt;span style=&#34;color:#f60&#34;&gt;000&lt;/span&gt; BELL
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;  Assets&lt;span style=&#34;color:#555&#34;&gt;:&lt;/span&gt;Savings                 &lt;span style=&#34;color:#f60&#34;&gt;29&lt;/span&gt;,&lt;span style=&#34;color:#f60&#34;&gt;800&lt;/span&gt; BELL
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;  Assets&lt;span style=&#34;color:#555&#34;&gt;:&lt;/span&gt;Cash
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;Tagging transactions in these ways allows for powerful queries when reporting later on. Lloid can now easily build comprehensive reports for all transactions for a particular build, or across multiple builds. In the real world this type of tagging could be useful if you let out property. For example, you could tag all transactions involving the property (e.g. rent, expenses, mortgage payments, etc.) to make tax returns much less painful.&lt;/p&gt;
&lt;h2 id=&#34;investments-and-capital-gains--losses&#34;&gt;Investments and capital gains (&amp; losses)&lt;/h2&gt;
&lt;p&gt;Each Sunday morning your island will be visited by &lt;a href=&#34;https://animalcrossing.fandom.com/wiki/Daisy_Mae&#34; target=&#34;_blank&#34; rel=&#34;noopener noreferrer&#34; &gt;Daisy Mae&lt;/a&gt;, who sells her family’s special turnips. The main focus of this feature is to take part in the “Stalk Market”, which is arguably one of the quickest ways to accumulate Bells in the game.&lt;/p&gt;
&lt;p&gt;The player buys the turnips with Bells (the exchange rate for which varies every week, but is usually at around 100 Bells per turnip). Then, during the week, Nook’s Cranny can buy the turnips from you. Nook’s Cranny’s turnip prices change twice per day, and can vary wildly. The goal of the player is therefore to try to sell the turnips at a greater price than what they were bought for.&lt;/p&gt;
&lt;p&gt;&lt;img src=&#34;https://wilw.dev/media/blog/acnh8.png&#34; alt=&#34;&#34;&gt;&lt;/p&gt;
&lt;p&gt;There is a catch, however, for if the player does not sell the turnips within a week, they rot and can then only ever be sold at a fraction of what was initially paid for them.&lt;/p&gt;
&lt;p&gt;Ledger has first-class support for understanding investments like these. Let’s say that we buy 100 turnips at a price of 100 Bells each:&lt;/p&gt;
&lt;div class=&#34;highlight&#34;&gt;&lt;pre tabindex=&#34;0&#34; style=&#34;background-color:#f0f3f3;-moz-tab-size:2;-o-tab-size:2;tab-size:2;&#34;&gt;&lt;code class=&#34;language-jsx&#34; data-lang=&#34;jsx&#34;&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;&lt;span style=&#34;color:#f60&#34;&gt;2020&lt;/span&gt;&lt;span style=&#34;color:#555&#34;&gt;/&lt;/span&gt;&lt;span style=&#34;color:#f60&#34;&gt;06&lt;/span&gt;&lt;span style=&#34;color:#555&#34;&gt;/&lt;/span&gt;&lt;span style=&#34;color:#f60&#34;&gt;07&lt;/span&gt;  Daisy Mae
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;  Assets&lt;span style=&#34;color:#555&#34;&gt;:&lt;/span&gt;Investments  &lt;span style=&#34;color:#f60&#34;&gt;100&lt;/span&gt; TURNIP &lt;span style=&#34;color:#a00;background-color:#faa&#34;&gt;@&lt;/span&gt; &lt;span style=&#34;color:#f60&#34;&gt;100&lt;/span&gt; BELL
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;  Assets&lt;span style=&#34;color:#555&#34;&gt;:&lt;/span&gt;Cash
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;Ledger will automatically calculate the Bell cost based on the turnip volume and lot price, and will credit the cash account accordingly to balance.&lt;/p&gt;
&lt;p&gt;When buying turnips from Daisy Mae, despite them being a type of food that you can eat in the game, they are usually considered a non-edible commodity. As such we treat them as just another type of asset that we still own (e.g. similar to buying oil, gold, or stock in a company). If you were to buy the turnips for purposes other than selling later then you’d probably treat the transaction as an expense instead.&lt;/p&gt;
&lt;p&gt;If we now run a balance report of our assets (we include liabilities here so we don’t forget about what we still also owe), we can see Ledger accounting for all of our different commodities:&lt;/p&gt;
&lt;div class=&#34;highlight&#34;&gt;&lt;pre tabindex=&#34;0&#34; style=&#34;background-color:#f0f3f3;-moz-tab-size:2;-o-tab-size:2;tab-size:2;&#34;&gt;&lt;code class=&#34;language-jsx&#34; data-lang=&#34;jsx&#34;&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;&lt;span style=&#34;color:#555&#34;&gt;~&lt;/span&gt; ledger bal Assets Liabilities
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;        &lt;span style=&#34;color:#f60&#34;&gt;132&lt;/span&gt;,&lt;span style=&#34;color:#f60&#34;&gt;000&lt;/span&gt; BELL
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;           &lt;span style=&#34;color:#f60&#34;&gt;1200&lt;/span&gt; MILE
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;          &lt;span style=&#34;color:#f60&#34;&gt;100&lt;/span&gt; TURNIP  Assets
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;         &lt;span style=&#34;color:#f60&#34;&gt;26&lt;/span&gt;,&lt;span style=&#34;color:#f60&#34;&gt;000&lt;/span&gt; BELL    Cash
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;          &lt;span style=&#34;color:#f60&#34;&gt;100&lt;/span&gt; TURNIP    Investments
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;           &lt;span style=&#34;color:#f60&#34;&gt;1200&lt;/span&gt; MILE    NookMiles
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;         &lt;span style=&#34;color:#f60&#34;&gt;98&lt;/span&gt;,&lt;span style=&#34;color:#f60&#34;&gt;000&lt;/span&gt; BELL    Property
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;          &lt;span style=&#34;color:#f60&#34;&gt;8&lt;/span&gt;,&lt;span style=&#34;color:#f60&#34;&gt;000&lt;/span&gt; BELL    Savings
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;        &lt;span style=&#34;color:#555&#34;&gt;-&lt;/span&gt;&lt;span style=&#34;color:#f60&#34;&gt;94&lt;/span&gt;,&lt;span style=&#34;color:#f60&#34;&gt;000&lt;/span&gt; BELL  Liabilities&lt;span style=&#34;color:#555&#34;&gt;:&lt;/span&gt;NookInc
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;&lt;span style=&#34;color:#555&#34;&gt;--------------------&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;         &lt;span style=&#34;color:#f60&#34;&gt;38&lt;/span&gt;,&lt;span style=&#34;color:#f60&#34;&gt;000&lt;/span&gt; BELL
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;           &lt;span style=&#34;color:#f60&#34;&gt;1200&lt;/span&gt; MILE
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;          &lt;span style=&#34;color:#f60&#34;&gt;100&lt;/span&gt; TURNIP
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;The plan now is to try and sell the turnips at a lot price of more than 100 Bells, and so we check at Nook’s Cranny each day for the current turnip price.&lt;/p&gt;
&lt;p&gt;&lt;img src=&#34;https://wilw.dev/media/blog/acnh9.png&#34; alt=&#34;&#34;&gt;&lt;/p&gt;
&lt;p&gt;Eventually, we find a price we are happy with (e.g. 200 Bells), and we sell all of our turnips. Since Ledger doesn’t let us create Bells out of nowhere, to indicate this appreciation (we would now have twice as many as we did before) we need to make a credit in an income account to represent the capital gains:&lt;/p&gt;
&lt;div class=&#34;highlight&#34;&gt;&lt;pre tabindex=&#34;0&#34; style=&#34;background-color:#f0f3f3;-moz-tab-size:2;-o-tab-size:2;tab-size:2;&#34;&gt;&lt;code class=&#34;language-jsx&#34; data-lang=&#34;jsx&#34;&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;&lt;span style=&#34;color:#f60&#34;&gt;2020&lt;/span&gt;&lt;span style=&#34;color:#555&#34;&gt;/&lt;/span&gt;&lt;span style=&#34;color:#f60&#34;&gt;06&lt;/span&gt;&lt;span style=&#34;color:#555&#34;&gt;/&lt;/span&gt;&lt;span style=&#34;color:#f60&#34;&gt;06&lt;/span&gt;  Nook&lt;span style=&#34;color:#a00;background-color:#faa&#34;&gt;&#39;&lt;/span&gt;s Cranny
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;  Assets&lt;span style=&#34;color:#555&#34;&gt;:&lt;/span&gt;Cash         &lt;span style=&#34;color:#f60&#34;&gt;20&lt;/span&gt;,&lt;span style=&#34;color:#f60&#34;&gt;000&lt;/span&gt; BELL
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;  Assets&lt;span style=&#34;color:#555&#34;&gt;:&lt;/span&gt;Investments  &lt;span style=&#34;color:#555&#34;&gt;-&lt;/span&gt;&lt;span style=&#34;color:#f60&#34;&gt;100&lt;/span&gt; TURNIP {&lt;span style=&#34;color:#f60&#34;&gt;100&lt;/span&gt; BELL} &lt;span style=&#34;color:#a00;background-color:#faa&#34;&gt;@&lt;/span&gt; &lt;span style=&#34;color:#f60&#34;&gt;200&lt;/span&gt; BELL
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;  Income&lt;span style=&#34;color:#555&#34;&gt;:&lt;/span&gt;Capital Gains
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;In Ledger, we indicate the cost that we originally bought the turnips for, as well as what we sold them for. As usual it will work out the capital gains for us. If we were instead to make a loss on our turnips, we would debit our cash account with whatever we &lt;em&gt;did&lt;/em&gt; make back and also an equity account to indicate a loss in our net worth.&lt;/p&gt;
&lt;p&gt;Now, if we run a profit and loss report for the past few days, we would see that we are now in profit!&lt;/p&gt;
&lt;div class=&#34;highlight&#34;&gt;&lt;pre tabindex=&#34;0&#34; style=&#34;background-color:#f0f3f3;-moz-tab-size:2;-o-tab-size:2;tab-size:2;&#34;&gt;&lt;code class=&#34;language-jsx&#34; data-lang=&#34;jsx&#34;&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;&lt;span style=&#34;color:#555&#34;&gt;~&lt;/span&gt; ledger bal Income Expenses
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;         &lt;span style=&#34;color:#f60&#34;&gt;28&lt;/span&gt;,&lt;span style=&#34;color:#f60&#34;&gt;000&lt;/span&gt; BELL
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;           &lt;span style=&#34;color:#f60&#34;&gt;5000&lt;/span&gt; MILE  Expenses
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;          &lt;span style=&#34;color:#f60&#34;&gt;3&lt;/span&gt;,&lt;span style=&#34;color:#f60&#34;&gt;000&lt;/span&gt; BELL    Furniture
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;         &lt;span style=&#34;color:#f60&#34;&gt;25&lt;/span&gt;,&lt;span style=&#34;color:#f60&#34;&gt;000&lt;/span&gt; BELL    Infrastructure
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;           &lt;span style=&#34;color:#f60&#34;&gt;5000&lt;/span&gt; MILE    Travel
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;        &lt;span style=&#34;color:#555&#34;&gt;-&lt;/span&gt;&lt;span style=&#34;color:#f60&#34;&gt;86&lt;/span&gt;,&lt;span style=&#34;color:#f60&#34;&gt;000&lt;/span&gt; BELL
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;          &lt;span style=&#34;color:#555&#34;&gt;-&lt;/span&gt;&lt;span style=&#34;color:#f60&#34;&gt;6200&lt;/span&gt; MILE  Income
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;          &lt;span style=&#34;color:#555&#34;&gt;-&lt;/span&gt;&lt;span style=&#34;color:#f60&#34;&gt;6200&lt;/span&gt; MILE    Achievements
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;        &lt;span style=&#34;color:#555&#34;&gt;-&lt;/span&gt;&lt;span style=&#34;color:#f60&#34;&gt;10&lt;/span&gt;,&lt;span style=&#34;color:#f60&#34;&gt;000&lt;/span&gt; BELL    Capital Gains
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;        &lt;span style=&#34;color:#555&#34;&gt;-&lt;/span&gt;&lt;span style=&#34;color:#f60&#34;&gt;76&lt;/span&gt;,&lt;span style=&#34;color:#f60&#34;&gt;000&lt;/span&gt; BELL    Sales
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;&lt;span style=&#34;color:#555&#34;&gt;--------------------&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;        &lt;span style=&#34;color:#555&#34;&gt;-&lt;/span&gt;&lt;span style=&#34;color:#f60&#34;&gt;58&lt;/span&gt;,&lt;span style=&#34;color:#f60&#34;&gt;000&lt;/span&gt; BELL
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;          &lt;span style=&#34;color:#555&#34;&gt;-&lt;/span&gt;&lt;span style=&#34;color:#f60&#34;&gt;1200&lt;/span&gt; MILE
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;Ledger allows you to track the the prices for various commodities using a separate “price database” file, which provides an historical record of the prices for commodities in your portfolio, and will be automatically consulted by Ledger when running reports. Read more about this in &lt;a href=&#34;https://www.ledger-cli.org/3.0/doc/ledger3.html#Commodities-and-Currencies&#34; target=&#34;_blank&#34; rel=&#34;noopener noreferrer&#34; &gt;the documentation&lt;/a&gt;.&lt;/p&gt;
&lt;h2 id=&#34;currency-exchange&#34;&gt;“Currency” exchange&lt;/h2&gt;
&lt;p&gt;I won’t go into much detail here, since we’ve already covered exchanging assets a few times. However, the game provides in-built mechanisms for exchanging Nook Miles for Bells. From the Nook Stop one can buy &lt;a href=&#34;https://animalcrossing.fandom.com/wiki/Bell_voucher&#34; target=&#34;_blank&#34; rel=&#34;noopener noreferrer&#34; &gt;Bell vouchers&lt;/a&gt; for 500 Miles each, which provide a convenient way to convert your Miles to Bells.&lt;/p&gt;
&lt;p&gt;Please note: there is probably a more elegant approach to this process, so if you have any better ways of accounting for these transactions it’d be great to hear them!&lt;/p&gt;
&lt;p&gt;However, in my view, when we buy the vouchers we are essentially spending our own Miles:&lt;/p&gt;
&lt;div class=&#34;highlight&#34;&gt;&lt;pre tabindex=&#34;0&#34; style=&#34;background-color:#f0f3f3;-moz-tab-size:2;-o-tab-size:2;tab-size:2;&#34;&gt;&lt;code class=&#34;language-jsx&#34; data-lang=&#34;jsx&#34;&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;&lt;span style=&#34;color:#f60&#34;&gt;2020&lt;/span&gt;&lt;span style=&#34;color:#555&#34;&gt;/&lt;/span&gt;&lt;span style=&#34;color:#f60&#34;&gt;06&lt;/span&gt;&lt;span style=&#34;color:#555&#34;&gt;/&lt;/span&gt;&lt;span style=&#34;color:#f60&#34;&gt;07&lt;/span&gt;  Nook Stop
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;  Expenses&lt;span style=&#34;color:#555&#34;&gt;:&lt;/span&gt;NookInc  &lt;span style=&#34;color:#f60&#34;&gt;1000&lt;/span&gt; MILE
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;  Assets&lt;span style=&#34;color:#555&#34;&gt;:&lt;/span&gt;NookMiles  &lt;span style=&#34;color:#555&#34;&gt;-&lt;/span&gt;&lt;span style=&#34;color:#f60&#34;&gt;1000&lt;/span&gt; MILE
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;  Assets&lt;span style=&#34;color:#555&#34;&gt;:&lt;/span&gt;Vouchers   &lt;span style=&#34;color:#f60&#34;&gt;2&lt;/span&gt; BV &lt;span style=&#34;color:#a00;background-color:#faa&#34;&gt;@&lt;/span&gt; &lt;span style=&#34;color:#f60&#34;&gt;500&lt;/span&gt; MILE
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;  Income&lt;span style=&#34;color:#555&#34;&gt;:&lt;/span&gt;Vouchers   &lt;span style=&#34;color:#555&#34;&gt;-&lt;/span&gt;&lt;span style=&#34;color:#f60&#34;&gt;2&lt;/span&gt; BV &lt;span style=&#34;color:#a00;background-color:#faa&#34;&gt;@&lt;/span&gt; &lt;span style=&#34;color:#f60&#34;&gt;500&lt;/span&gt; MILE
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;And then, when we sell the vouchers in Nook’s Cranny we are spending the vouchers in return for 3,000 Bells each:&lt;/p&gt;
&lt;div class=&#34;highlight&#34;&gt;&lt;pre tabindex=&#34;0&#34; style=&#34;background-color:#f0f3f3;-moz-tab-size:2;-o-tab-size:2;tab-size:2;&#34;&gt;&lt;code class=&#34;language-jsx&#34; data-lang=&#34;jsx&#34;&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;&lt;span style=&#34;color:#f60&#34;&gt;2020&lt;/span&gt;&lt;span style=&#34;color:#555&#34;&gt;/&lt;/span&gt;&lt;span style=&#34;color:#f60&#34;&gt;06&lt;/span&gt;&lt;span style=&#34;color:#555&#34;&gt;/&lt;/span&gt;&lt;span style=&#34;color:#f60&#34;&gt;07&lt;/span&gt;  Nook&lt;span style=&#34;color:#a00;background-color:#faa&#34;&gt;&#39;&lt;/span&gt;s Cranny
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;  Expenses&lt;span style=&#34;color:#555&#34;&gt;:&lt;/span&gt;NookInc  &lt;span style=&#34;color:#f60&#34;&gt;2&lt;/span&gt; BV &lt;span style=&#34;color:#a00;background-color:#faa&#34;&gt;@&lt;/span&gt; &lt;span style=&#34;color:#f60&#34;&gt;3000&lt;/span&gt; BELL
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;  Assets&lt;span style=&#34;color:#555&#34;&gt;:&lt;/span&gt;Vouchers   &lt;span style=&#34;color:#555&#34;&gt;-&lt;/span&gt;&lt;span style=&#34;color:#f60&#34;&gt;2&lt;/span&gt; BV &lt;span style=&#34;color:#a00;background-color:#faa&#34;&gt;@&lt;/span&gt; &lt;span style=&#34;color:#f60&#34;&gt;3000&lt;/span&gt; BELL
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;  Assets&lt;span style=&#34;color:#555&#34;&gt;:&lt;/span&gt;Cash       &lt;span style=&#34;color:#f60&#34;&gt;3000&lt;/span&gt; BELL
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;  Income&lt;span style=&#34;color:#555&#34;&gt;:&lt;/span&gt;NookInc    &lt;span style=&#34;color:#555&#34;&gt;-&lt;/span&gt;&lt;span style=&#34;color:#f60&#34;&gt;3000&lt;/span&gt; BELL
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;This approach means our accounts remain balanced and we don’t have outstanding assets reported.&lt;/p&gt;
&lt;h2 id=&#34;reporting-balance-sheets--profit-and-losses&#34;&gt;Reporting balance sheets &amp; profit and losses&lt;/h2&gt;
&lt;p&gt;That’s all I’m going to cover in this post about recording transactions, but I will finish off with a couple of final useful types of reports using Ledger we can run now that we have a fuller set of accounts.&lt;/p&gt;
&lt;p&gt;When running reports it is often useful to scope to a particular time period - e.g. to focus on a particular set of transactions in a given tax year. Ledger nicely supports querying specific timeframes for transactions and balances. For example, to view our P&amp;L for only May:&lt;/p&gt;
&lt;div class=&#34;highlight&#34;&gt;&lt;pre tabindex=&#34;0&#34; style=&#34;background-color:#f0f3f3;-moz-tab-size:2;-o-tab-size:2;tab-size:2;&#34;&gt;&lt;code class=&#34;language-jsx&#34; data-lang=&#34;jsx&#34;&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;&lt;span style=&#34;color:#555&#34;&gt;~&lt;/span&gt; ledger bal Income Expenses &lt;span style=&#34;color:#555&#34;&gt;-&lt;/span&gt;b &lt;span style=&#34;color:#c30&#34;&gt;&#34;2020/05/01&#34;&lt;/span&gt; &lt;span style=&#34;color:#555&#34;&gt;-&lt;/span&gt;e &lt;span style=&#34;color:#c30&#34;&gt;&#34;2020/06/01&#34;&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;         &lt;span style=&#34;color:#f60&#34;&gt;23&lt;/span&gt;,&lt;span style=&#34;color:#f60&#34;&gt;000&lt;/span&gt; BELL
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;           &lt;span style=&#34;color:#f60&#34;&gt;5000&lt;/span&gt; MILE  Expenses
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;          &lt;span style=&#34;color:#f60&#34;&gt;3&lt;/span&gt;,&lt;span style=&#34;color:#f60&#34;&gt;000&lt;/span&gt; BELL    Furniture
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;         &lt;span style=&#34;color:#f60&#34;&gt;20&lt;/span&gt;,&lt;span style=&#34;color:#f60&#34;&gt;000&lt;/span&gt; BELL    Infrastructure
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;           &lt;span style=&#34;color:#f60&#34;&gt;5000&lt;/span&gt; MILE    Travel
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;        &lt;span style=&#34;color:#555&#34;&gt;-&lt;/span&gt;&lt;span style=&#34;color:#f60&#34;&gt;76&lt;/span&gt;,&lt;span style=&#34;color:#f60&#34;&gt;000&lt;/span&gt; BELL
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;          &lt;span style=&#34;color:#555&#34;&gt;-&lt;/span&gt;&lt;span style=&#34;color:#f60&#34;&gt;6200&lt;/span&gt; MILE  Income
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;          &lt;span style=&#34;color:#555&#34;&gt;-&lt;/span&gt;&lt;span style=&#34;color:#f60&#34;&gt;6200&lt;/span&gt; MILE    Achievements
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;        &lt;span style=&#34;color:#555&#34;&gt;-&lt;/span&gt;&lt;span style=&#34;color:#f60&#34;&gt;76&lt;/span&gt;,&lt;span style=&#34;color:#f60&#34;&gt;000&lt;/span&gt; BELL    Sales
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;&lt;span style=&#34;color:#555&#34;&gt;--------------------&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;        &lt;span style=&#34;color:#555&#34;&gt;-&lt;/span&gt;&lt;span style=&#34;color:#f60&#34;&gt;53&lt;/span&gt;,&lt;span style=&#34;color:#f60&#34;&gt;000&lt;/span&gt; BELL
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;          &lt;span style=&#34;color:#555&#34;&gt;-&lt;/span&gt;&lt;span style=&#34;color:#f60&#34;&gt;1200&lt;/span&gt; MILE
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;Or to view only the transactions involving &lt;code&gt;bridge1&lt;/code&gt; from June onwards:&lt;/p&gt;
&lt;div class=&#34;highlight&#34;&gt;&lt;pre tabindex=&#34;0&#34; style=&#34;background-color:#f0f3f3;-moz-tab-size:2;-o-tab-size:2;tab-size:2;&#34;&gt;&lt;code class=&#34;language-jsx&#34; data-lang=&#34;jsx&#34;&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;&lt;span style=&#34;color:#555&#34;&gt;~&lt;/span&gt; ledger reg &lt;span style=&#34;color:#555&#34;&gt;%&lt;/span&gt;bridge1 &lt;span style=&#34;color:#555&#34;&gt;-&lt;/span&gt;b &lt;span style=&#34;color:#c30&#34;&gt;&#34;June 2020&#34;&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;&lt;span style=&#34;color:#f60&#34;&gt;20&lt;/span&gt;&lt;span style=&#34;color:#555&#34;&gt;-&lt;/span&gt;Jun&lt;span style=&#34;color:#555&#34;&gt;-&lt;/span&gt;&lt;span style=&#34;color:#f60&#34;&gt;01&lt;/span&gt; Lloid                 Expenses&lt;span style=&#34;color:#555&#34;&gt;:&lt;/span&gt;Infrastructure    &lt;span style=&#34;color:#f60&#34;&gt;5&lt;/span&gt;,&lt;span style=&#34;color:#f60&#34;&gt;000&lt;/span&gt; BELL    &lt;span style=&#34;color:#f60&#34;&gt;5&lt;/span&gt;,&lt;span style=&#34;color:#f60&#34;&gt;000&lt;/span&gt; BELL
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;                                Assets&lt;span style=&#34;color:#555&#34;&gt;:&lt;/span&gt;Cash               &lt;span style=&#34;color:#555&#34;&gt;-&lt;/span&gt;&lt;span style=&#34;color:#f60&#34;&gt;5&lt;/span&gt;,&lt;span style=&#34;color:#f60&#34;&gt;000&lt;/span&gt; BELL             &lt;span style=&#34;color:#f60&#34;&gt;0&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;If I wanted to see all capital gains income for the year-to-date (e.g. for calculating capital gains tax):&lt;/p&gt;
&lt;div class=&#34;highlight&#34;&gt;&lt;pre tabindex=&#34;0&#34; style=&#34;background-color:#f0f3f3;-moz-tab-size:2;-o-tab-size:2;tab-size:2;&#34;&gt;&lt;code class=&#34;language-jsx&#34; data-lang=&#34;jsx&#34;&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;&lt;span style=&#34;color:#555&#34;&gt;~&lt;/span&gt; ledger bal &lt;span style=&#34;color:#c30&#34;&gt;&#34;Capital Gains&#34;&lt;/span&gt; &lt;span style=&#34;color:#555&#34;&gt;-&lt;/span&gt;b &lt;span style=&#34;color:#c30&#34;&gt;&#34;January&#34;&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;        &lt;span style=&#34;color:#555&#34;&gt;-&lt;/span&gt;&lt;span style=&#34;color:#f60&#34;&gt;10&lt;/span&gt;,&lt;span style=&#34;color:#f60&#34;&gt;000&lt;/span&gt; BELL  Income&lt;span style=&#34;color:#555&#34;&gt;:&lt;/span&gt;Capital Gains
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;Again, I’ll mention the &lt;a href=&#34;https://www.ledger-cli.org/3.0/doc/ledger3.html&#34; target=&#34;_blank&#34; rel=&#34;noopener noreferrer&#34; &gt;Ledger documentation&lt;/a&gt;, which goes into reporting in much more detail.&lt;/p&gt;
&lt;h2 id=&#34;conclusion&#34;&gt;Conclusion&lt;/h2&gt;
&lt;p&gt;I hope this has been a useful insight into how command-line accounting can work and how it might be beneficial when using it for managing your own real-world (or non-real!) finances.&lt;/p&gt;
&lt;p&gt;Although there are definitely “wrong” ways to do bookkeeping, as long as you follow the basic principles then the way you keep your books is down to what works for you. If this is something you want to try yourself then, like a diet, it’s important to find an approach that means you can stick to it - by choosing a set of accounts that gives enough detail without being too specific to manage, and by finding the right time balance spent in logging your transactions and reconciling against your real world accounts.&lt;/p&gt;
&lt;p&gt;I try to update my personal ledger every few days, and now find it much quicker to simply update the text file than navigating around GUI tools and Xero - even with their fancy auto-reconciliation. Running reports is lightning fast and having a single text file as a source-of-truth means it can easily be synced between devices for editing, or checked into source control.&lt;/p&gt;
&lt;p&gt;I’ve been using Ledger for a while now (I don’t have a very complicated financial life), and I’ve found myself agreeing with others who have tried it - I feel that I understand my finances more thoroughly, and it is hugely satisfying adding postings to my ledger and observing how this affects the accounts and the reflections to their real-world counterparts.&lt;/p&gt;
</content:encoded>
    </item>
    
    <item>
      <title>Kubernetes Cluster: Essentials</title>
      <link>https://wilw.dev/blog/2020/02/02/kube-cluster/</link>
      <pubDate>Sun, 02 Feb 2020 17:00:00 +0000</pubDate>
      
      <guid>https://wilw.dev/blog/2020/02/02/kube-cluster/</guid>
      
        <category>kubernetes</category>
      
        <category>devops</category>
      
        <category>technology</category>
      
      
      <content:encoded>&lt;p&gt;This note documents the set-up of a k8s cluster from scratch, including ingress and load-balanced TLS support for web applications. It’s mainly for myself to revisit and reference later on. The result of this note is not (quite) production-grade, and additional features (e.g. firewalls/logging/backups) should be enabled to improve its robustness.&lt;/p&gt;
&lt;p&gt;Several cloud providers offer managed k8s services (including &lt;a href=&#34;https://aws.amazon.com/eks/&#34; target=&#34;_blank&#34; rel=&#34;noopener noreferrer&#34; &gt;Amazon EKS&lt;/a&gt;, &lt;a href=&#34;https://cloud.google.com/kubernetes-engine/&#34; target=&#34;_blank&#34; rel=&#34;noopener noreferrer&#34; &gt;GKE&lt;/a&gt;, &lt;a href=&#34;https://www.digitalocean.com/products/kubernetes/&#34; target=&#34;_blank&#34; rel=&#34;noopener noreferrer&#34; &gt;Digital Ocean&lt;/a&gt;, etc.). Whilst these would be recommended for sensitive or production workloads, I wanted to create my own provider-independent cluster in order to understand the ins and outs.&lt;/p&gt;
&lt;h2 id=&#34;infrastructure&#34;&gt;Infrastructure&lt;/h2&gt;
&lt;p&gt;Create the underlying infrastructure for the cluster by provisioning at least two new instances - all with at least 2GB memory and 2 CPUs. I used the latest Ubuntu LTS.&lt;/p&gt;
&lt;p&gt;One of the new instances (&lt;code&gt;kube1&lt;/code&gt;) will be the “control” node, and the others (&lt;code&gt;kube2&lt;/code&gt;&#43;) will be the “worker” nodes. In this scenario the “control plane” is made up of just one node - &lt;code&gt;kube1&lt;/code&gt;.&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;In larger clusters the control plane can be made to be highly available by using multiple control nodes in different availability zones.&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;Enable private networking between the nodes and ensure they can all access the internet using a public IP or through another interface (e.g. a NAT). If you use firewalls ensure the nodes can &lt;a href=&#34;https://kubernetes.io/docs/setup/production-environment/tools/kubeadm/install-kubeadm/&#34; target=&#34;_blank&#34; rel=&#34;noopener noreferrer&#34; &gt;communicate with each other via their private IPs&lt;/a&gt;.&lt;/p&gt;
&lt;h2 id=&#34;dependencies&#34;&gt;Dependencies&lt;/h2&gt;
&lt;p&gt;On all nodes (control and workers), install Docker and k8s binaries:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;Add Google apt keys: &lt;code&gt;wget -qO - https://packages.cloud.google.com/apt/doc/apt-key.gpg | sudo apt-key add -&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;Add Google repo: &lt;code&gt;add-apt-repository ‘deb http://apt.kubernetes.io/ kubernetes-bionic main&#39;&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;Update local repos: &lt;code&gt;apt update&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;Install docker.io, kubelet, and kubeadm: &lt;code&gt;apt install docker.io kubelet kubeadm&lt;/code&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;On the control node only (&lt;code&gt;kube1&lt;/code&gt;) also:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;Install kubectl: &lt;code&gt;apt install kubectl&lt;/code&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;h2 id=&#34;set-up-the-cluster&#34;&gt;Set-up the cluster&lt;/h2&gt;
&lt;p&gt;On the control node (&lt;code&gt;kube1&lt;/code&gt;) prepare the cluster:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;As root, initialise the cluster: &lt;code&gt;kubeadm init --pod-network-cidr=10.244.0.0/16 --apiserver-advertise-address=10.131.106.38&lt;/code&gt;
&lt;ul&gt;
&lt;li&gt;Replace apiserver-advertise-address with the private IP for the master node&lt;/li&gt;
&lt;li&gt;Setting &lt;code&gt;--pod-network-cidr&lt;/code&gt; allows Flannel (the CNI plugin we’re using here) to later allocate IP addresses to pods in your cluster from this range&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;Create a new user to manage kubernetes: &lt;code&gt;useradd kubeuser&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;Create a new &lt;code&gt;.kube&lt;/code&gt; directory: &lt;code&gt;mkdir /home/kubeuser/.kube &amp;&amp; chown kubeuser:kubeuser /home/kubeuser/.kube&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;Copy in the default config file: &lt;code&gt;cp /etc/kubernetes/admin.conf /home/kubeuser/.kube/config &amp;&amp; chown kubeuser:kubeuser /home/kubeuser/.kube/config&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;As the kubeuser user, install the Flannel network fabric by apply the appropriate YAML: &lt;code&gt;kubectl apply -f kubectl apply -f https://raw.githubusercontent.com/coreos/flannel/master/Documentation/kube-flannel.yml&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;Finally, back as root, get and copy the kubernetes join command: &lt;code&gt;kubeadm token create --print-join-command&lt;/code&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;Now join the worker nodes to the new cluster:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;As root, copy and paste and run the output from the last command on the control node on each worker node.&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;After a few minutes &lt;code&gt;kubectl get nodes&lt;/code&gt; (as the &lt;code&gt;kubeuser&lt;/code&gt; user on the control node) should list the other nodes in the cluster.&lt;/p&gt;
&lt;p&gt;From now on, all &lt;code&gt;kubectl&lt;/code&gt; commands should be issued by &lt;code&gt;kubeuser&lt;/code&gt; on the control node, and all &lt;code&gt;kubeadm&lt;/code&gt; commands by &lt;code&gt;root&lt;/code&gt; on the control or worker nodes.&lt;/p&gt;
&lt;p&gt;At this point you have a working cluster ready to create pods, deployments and services. The next sections go on to set up the cluster to allow exposing, securing, and load-balancing services for access from outside the cluster.&lt;/p&gt;
&lt;h2 id=&#34;install-helm&#34;&gt;Install Helm&lt;/h2&gt;
&lt;p&gt;It’s a good idea to install Helm (a good way to install more complex packages and services on your cluster) according to &lt;a href=&#34;https://helm.sh/docs/intro/quickstart&#34; target=&#34;_blank&#34; rel=&#34;noopener noreferrer&#34; &gt;the official website&lt;/a&gt;.&lt;/p&gt;
&lt;h2 id=&#34;load-balancing-and-ingress&#34;&gt;Load-balancing and ingress&lt;/h2&gt;
&lt;p&gt;We’re now ready to route external traffic to the cluster.&lt;/p&gt;
&lt;h3 id=&#34;create-a-load-balancer&#34;&gt;Create a load balancer&lt;/h3&gt;
&lt;p&gt;Use your provider’s load balancer service (or create your own load balancer using a reverse-proxy like Nginx) and make a note of the IP address (or host name) of the load balancer server. Add your kubernetes worker nodes to your load balancer’s distribution group.&lt;/p&gt;
&lt;p&gt;If you have a domain name you want to associate with a service on your cluster, update your DNS settings to point the domain to the load balancer.&lt;/p&gt;
&lt;p&gt;Next install the &lt;code&gt;nginx-ingress&lt;/code&gt; k8s ingress controller via Helm (inserting the IP address of your load balancer).&lt;/p&gt;
&lt;pre tabindex=&#34;0&#34;&gt;&lt;code&gt;helm install nginx-ingress stable/nginx-ingress \
--set controller.replicaCount=2 \
--set controller.nodeSelector.&#34;beta\.kubernetes\.io/os&#34;=linux \
--set defaultBackend.nodeSelector.&#34;beta\.kubernetes\.io/os&#34;=linux \
--set controller.service.loadBalancerIP=“&lt;LOAD BALANCER IP&gt;”
&lt;/code&gt;&lt;/pre&gt;&lt;p&gt;Check the services (&lt;code&gt;kubectl get services&lt;/code&gt;) now running and note the HTTP/HTTPS ports used by the new ingress controller. Update your load balancer to use these ports as the forwarding ports in the load balancer settings (e.g. 80 -&gt; 32345, 443 -&gt; 31454).&lt;/p&gt;
&lt;p&gt;Finally create an ingress resource for the domain/service by applying the following (replacing the relevant details for your domain name (host) and the service name and port requests should be routed to).&lt;/p&gt;
&lt;pre tabindex=&#34;0&#34;&gt;&lt;code&gt;apiVersion: extensions/v1beta1
kind: Ingress
metadata:
name: primary-ingress
annotations:
  kubernetes.io/ingress.class: nginx
spec:
  rules:
    - host: &lt;DOMAIN&gt;
    http:
      paths:
          - backend:
            serviceName: &lt;SERVICE NAME&gt;
            servicePort: &lt;SERVIE PORT&gt;
    - http:
      paths:
          - backend:
            serviceName: nginx # this is default
            servicePort: 80
&lt;/code&gt;&lt;/pre&gt;&lt;p&gt;This ingress forwards all requests to the host &lt;code&gt;&lt;DOMAIN&gt;&lt;/code&gt; through to &lt;code&gt;&lt;SERVICE NAME&gt;&lt;/code&gt; on &lt;code&gt;&lt;SERVICE PORT&gt;&lt;/code&gt;. All non-matched requests (i.e. to other domains) will be sent to the default backend - in this case a simple &lt;code&gt;nginx&lt;/code&gt; service that returns 200 response codes (this is important to ensure load balancer health-checks pass).&lt;/p&gt;
&lt;p&gt;Visiting &lt;code&gt;&lt;DOMAIN&gt;&lt;/code&gt; in your web browser should now enable you to reach the service.&lt;/p&gt;
&lt;h3 id=&#34;securing-your-ingress&#34;&gt;Securing your ingress&lt;/h3&gt;
&lt;p&gt;Currently the ingress we created only accepts traffic over HTTP on port 80. To secure traffic (i.e. use HTTPS to access services), either terminate TLS at your load balancer and add relevant certificates there, or allow your cluster to handle the TLS connection, as described below.&lt;/p&gt;
&lt;p&gt;You can add HTTPS for services you want to expose through your load balancer. To begin, ensure that the domain is pointing to your load balancer (as above), and that you can reach your service through the load balancer using your domain name. Finally ensure that port 443 traffic to your load balancer is routed to the HTTPS port of the ingress controller service.&lt;/p&gt;
&lt;p&gt;We use &lt;code&gt;cert-manager&lt;/code&gt; to automatically manage certificates for us through LetsEncrypt. Install &lt;code&gt;cert-manager&lt;/code&gt;:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;Create a new namespace: &lt;code&gt;kubectl create namespace cert-manager&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;Apply the needed definitions: &lt;code&gt;kubectl apply --validate=false -f https://github.com/jetstack/cert-manager/releases/download/v0.13.0/cert-manager.yaml&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;Verify pods come online (the &lt;code&gt;webhook&lt;/code&gt; one may take longer): &lt;code&gt;kubectl get pods --namespace cert-manager&lt;/code&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;Once installed, cert-manager is formed from Issuer objects (determines how and where certificates should be issued from) and Certificate objects (determines the Secrets which contain the certificate values - these can be autorenewed by &lt;code&gt;cert-manager&lt;/code&gt;).&lt;/p&gt;
&lt;p&gt;First, create an issuer that uses letsencrypt’s production ACME service:&lt;/p&gt;
&lt;pre tabindex=&#34;0&#34;&gt;&lt;code&gt;apiVersion: cert-manager.io/v1alpha2
kind: Issuer
metadata:
  name: letsencrypt-prod
  namespace: default
spec:
  acme:
    server: https://acme-v02.api.letsencrypt.org/directory
    email: &lt;YOUR EMAIL&gt;
    privateKeySecretRef:
      name: letsencrypt-prod
    solvers:
    # An empty &#39;selector&#39; means that this solver matches all domains
    - selector: {}
      http01:
        ingress:
          class: nginx
&lt;/code&gt;&lt;/pre&gt;&lt;p&gt;Next, create a certificate that uses this issuer:&lt;/p&gt;
&lt;pre tabindex=&#34;0&#34;&gt;&lt;code&gt;apiVersion: cert-manager.io/v1alpha2
kind: Certificate
metadata:
  name: certificate-prod
  namespace: default
spec:
  secretName: domain-tls
  issuerRef:
    name: letsencrypt-prod
  commonName: &lt;DOMAIN&gt;
  dnsNames:
  - &lt;OTHER DOMAINS&gt;
&lt;/code&gt;&lt;/pre&gt;&lt;p&gt;Finally, update the ingress we created earlier to add a &lt;code&gt;tls&lt;/code&gt; section that references the secret created by the Certificate:&lt;/p&gt;
&lt;pre tabindex=&#34;0&#34;&gt;&lt;code&gt;...
spec:
  tls:
  - hosts:
    - &lt;DOMAIN&gt;
    secretName: domain-tls
  rules:
  - host: &lt;DOMAIN&gt;
    http: ...
&lt;/code&gt;&lt;/pre&gt;&lt;p&gt;After a few seconds your certificate should be issued and your service will be available via HTTPS through the load balancer.&lt;/p&gt;
</content:encoded>
    </item>
    
    <item>
      <title>Go backends on Now</title>
      <link>https://wilw.dev/blog/2019/08/20/go-now/</link>
      <pubDate>Tue, 20 Aug 2019 17:00:00 +0000</pubDate>
      
      <guid>https://wilw.dev/blog/2019/08/20/go-now/</guid>
      
        <category>golang</category>
      
        <category>vercel</category>
      
        <category>technology</category>
      
      
      <content:encoded>&lt;p&gt;ZEIT’s &lt;a href=&#34;https://zeit.co/now&#34; target=&#34;_blank&#34; rel=&#34;noopener noreferrer&#34; &gt;Now&lt;/a&gt; service is great for deploying apps and APIs that are able to make use of serverless execution models, and I use it for many of my projects (including this website, at the time of writing).&lt;/p&gt;
&lt;p&gt;I recently needed to deploy a backend written in Go and kept running into problems when trying to read data from the HTTP request body. The client-side app I was developing to communicate with the backend is also written in Go and everything seemed to work fine when running the backend locally (using &lt;code&gt;now dev&lt;/code&gt;), but the exact same requests failed when running it in production. The client’s request body was available when in development, but returned empty strings when running in production.&lt;/p&gt;
&lt;p&gt;It eventually boiled down to two things:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;Now’s dev environment does not seem to care about discrepancies between the &lt;code&gt;Content-Length&lt;/code&gt; header and the actual request body. In production, however, if the &lt;code&gt;Content-Length&lt;/code&gt; does not exist (or is &lt;code&gt;0&lt;/code&gt;) then the request body will not be read even if it exists (and I admit that this should probably be the expected behaviour and is probably a symptom of the underlying cloud architecture rather than Now itself).&lt;/li&gt;
&lt;li&gt;When creating new client requests, Go’s &lt;a href=&#34;https://golang.org/pkg/net/http/#Request&#34; target=&#34;_blank&#34; rel=&#34;noopener noreferrer&#34; &gt;&lt;code&gt;http&lt;/code&gt; package&lt;/a&gt; automatically sets the &lt;code&gt;Content-Length&lt;/code&gt; header based on the length of the request body (and will overwrite/remove the header even if it is set manually), but it will only do this under &lt;a href=&#34;https://golang.org/pkg/net/http/#NewRequest&#34; target=&#34;_blank&#34; rel=&#34;noopener noreferrer&#34; &gt;certain circumstances&lt;/a&gt;. In my scenario, a request was being generated with a valid body but without the &lt;code&gt;Content-Length&lt;/code&gt; header. To fix the issue, rather than passing in an instance of &lt;code&gt;os.File&lt;/code&gt; directly (which I was using to form my request), I needed to read the contents into a buffer before passing it into the request.&lt;/li&gt;
&lt;/ul&gt;
</content:encoded>
    </item>
    
    <item>
      <title>Hue: Security Lights</title>
      <link>https://wilw.dev/blog/2017/08/18/security-lights/</link>
      <pubDate>Fri, 18 Aug 2017 17:00:00 +0000</pubDate>
      
      <guid>https://wilw.dev/blog/2017/08/18/security-lights/</guid>
      
        <category>hue</category>
      
        <category>iot</category>
      
        <category>project</category>
      
        <category>technology</category>
      
      
      <content:encoded>&lt;p&gt;A &lt;a href=&#34;https://wilw.dev/blog/2017/06/26/cenode-iot&#34;&gt;previous note about Philips Hue bulbs&lt;/a&gt; got me thinking that the API exposed by the bridge might be used to warn if the house lights are left on too late at night, or even if they get turned on at unexpected times - potentially for security.&lt;/p&gt;
&lt;p&gt;I put together a simple program that periodically checks the status of known Hue bulbs late at night. If any bulbs are discovered to be powered on during such times then an email notification is sent. It runs as a &lt;code&gt;systemd&lt;/code&gt; service on a Raspberry Pi.&lt;/p&gt;
&lt;p&gt;&lt;img src=&#34;https://wilw.dev/media/blog/security-lights.png&#34; alt=&#34;Security lights&#34;&gt;&lt;/p&gt;
&lt;p&gt;Currently the project is quite basic, but it could be further extended - perhaps to implement ignore lists or to automatically turn off specific sets of bulbs if they are found to be powered on.&lt;/p&gt;
&lt;p&gt;For those interested, the project source and setup info is &lt;a href=&#34;https://github.com/willwebberley/lights-checker&#34; target=&#34;_blank&#34; rel=&#34;noopener noreferrer&#34; &gt;available on GitHub&lt;/a&gt;.&lt;/p&gt;
</content:encoded>
    </item>
    
    <item>
      <title>Alexa, ask Sherlock...</title>
      <link>https://wilw.dev/blog/2017/07/19/cenode-alexa/</link>
      <pubDate>Wed, 19 Jul 2017 17:00:00 +0000</pubDate>
      
      <guid>https://wilw.dev/blog/2017/07/19/cenode-alexa/</guid>
      
        <category>cenode</category>
      
        <category>alexa</category>
      
        <category>iot</category>
      
        <category>project</category>
      
      
      <content:encoded>&lt;p&gt;I have recently &lt;a href=&#34;https://wilw.dev/2017/06/22/cenode/&#34;&gt;posted about CENode&lt;/a&gt; and how it might be &lt;a href=&#34;https://wilw.dev/2017/06/26/cenode-iot/&#34;&gt;used in IoT systems&lt;/a&gt;.&lt;/p&gt;
&lt;p&gt;Since CENode is partially designed to communicate directly with humans (particularly those out and about or “in the field”) it makes sense for inputs and queries to be provided via voice in addition to or instead of a text interface. Whilst this has been explored in the browser (including in the &lt;a href=&#34;https://wilw.dev/2017/06/26/cenode-iot/&#34;&gt;previous Philips Hue control demo&lt;/a&gt;), it made sense to also try to leverage the Alexa voice service to interact with a CENode instance.&lt;/p&gt;
&lt;p&gt;The &lt;a href=&#34;https://developer.amazon.com/alexa-voice-service&#34; target=&#34;_blank&#34; rel=&#34;noopener noreferrer&#34; &gt;Alexa Voice Service&lt;/a&gt; and &lt;a href=&#34;https://developer.amazon.com/alexa-skills-kit&#34; target=&#34;_blank&#34; rel=&#34;noopener noreferrer&#34; &gt;Alexa Skills Kit&lt;/a&gt; are great to work with, and it was relatively straight forward to create a skill to communicate with CENode’s &lt;a href=&#34;https://github.com/willwebberley/CENode/wiki/CEServer-Usage&#34; target=&#34;_blank&#34; rel=&#34;noopener noreferrer&#34; &gt;RESTful API&lt;/a&gt;.&lt;/p&gt;
&lt;p&gt;&lt;a href=&#34;https://player.vimeo.com/video/226199106&#34; target=&#34;_blank&#34; rel=&#34;noopener noreferrer&#34; &gt;This short video&lt;/a&gt; demonstrates this through using an Amazon Echo to interact with a standard, non-modified CENode instance running on &lt;a href=&#34;http://explorer.cenode.io&#34; target=&#34;_blank&#34; rel=&#34;noopener noreferrer&#34; &gt;CENode Explorer&lt;/a&gt; that is partly pre-loaded with the “space” scenario used in our main &lt;a href=&#34;http://cenode.io/demo/index.html&#34; target=&#34;_blank&#34; rel=&#34;noopener noreferrer&#34; &gt;CENode demo&lt;/a&gt;. The rest of the post discusses the implementation and challenges.&lt;/p&gt;
&lt;p&gt;Typical Alexa skills are split into &lt;a href=&#34;https://developer.amazon.com/public/solutions/alexa/alexa-skills-kit/docs/alexa-skills-kit-interaction-model-reference&#34; target=&#34;_blank&#34; rel=&#34;noopener noreferrer&#34; &gt;“intents”&lt;/a&gt;, which describe the individual ways people might interact with the service. For example, the questions “what is the weather like today?” and “is it going to rain today?” may be two intents of a single weather skill.&lt;/p&gt;
&lt;p&gt;The skill logic is handled by &lt;a href=&#34;https://aws.amazon.com/lambda&#34; target=&#34;_blank&#34; rel=&#34;noopener noreferrer&#34; &gt;AWS Lambda&lt;/a&gt;, which is used to associate each intent with an action. When someone gives a voice command, the Alexa Voice Service (AVS) determines which intent is being called for which service, and then passes the control over to the appropriate segment in the Lambda function. The function returns a response to the AVS, which is read back out to the user.&lt;/p&gt;
&lt;p&gt;The strength of Alexa’s ability to recognise speech is largely dependent on the information given to build each intent. For example, the intent “what is the weather like in {cityName}?”, where &lt;code&gt;cityName&lt;/code&gt; is a variable with several different possibilities generated during the build, will accurately recognise speech initiating this intent because the sentence structure is so well defined. A single intent may have several ways of calling it - “what’s the weather like in…”, “tell me what
the weather is in…”, “what’s the weather forecast for…”, etc. - which can be bundled into the model to further improve the accuracy even in noisy environments or when spoken by people with strong accents.&lt;/p&gt;
&lt;p&gt;Since CENode is designed to work with an entire input string, however, the voice-to-text accuracy is much lower, and thus determining the intent and its arguments is harder. Since we need CENode to handle the entire input, our demo only has a single intent with two methods of invocation (slots):&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;code&gt;ask Sherlock {sentence}&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;&lt;code&gt;tell Sherlock {sentence}&lt;/code&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;Since ‘Sherlock’ is also provided as the invocation word for the service, both slots implicitly indicate both the service and the single intent to work with. I used ‘Sherlock’ as the name for the skill as it’s a name we’ve used before for CENode-related apps and it is an easy word for Alexa to understand!&lt;/p&gt;
&lt;p&gt;&lt;code&gt;sentence&lt;/code&gt; is the complete body to be processed by CENode - e.g. “Jupiter is a planet” or “what is Jupiter?” - giving a typical full Echo invocation: “Alexa, tell Sherlock Jupiter is a planet”. The &lt;code&gt;Alexa&lt;/code&gt; segment tells the Echo to begin listening, the &lt;code&gt;tell Sherlock&lt;/code&gt; component determines the skill and intent to use, and the remainder of the sentence is the body provided to CENode.&lt;/p&gt;
&lt;p&gt;Since we only have a single intent, using either ‘ask’ or ’tell’ in the invocation is irrelevant since it is CENode that will try and work out what is meant from the sentence body - whether a question or an input of information. The two slots are only used for the benefit of the human user and so invocations such as “tell Sherlock what is Jupiter?” still work.&lt;/p&gt;
&lt;p&gt;At this stage, the AWS Lambda function handling the intent makes a standard HTTP POST request to a CENode instance, and the response is directly passed back to the Alexa service for reading-out to the user. As such, CENode itself provides all of the error-handling and misunderstood inputs, making the Alexa service itself combined with the Lambda function, in this scenario, very ’thin’.&lt;/p&gt;
&lt;p&gt;&lt;img src=&#34;https://wilw.dev/media/blog/cenode-alexa.png&#34; alt=&#34;CENode and Alexa&#34;&gt;&lt;/p&gt;
&lt;p&gt;The skill has not yet been published to the Alexa skills store for general use, but the code for this project, including the Alexa Skills Kit configuration and the AWS Lambda code (written using their Node environment) is &lt;a href=&#34;https://github.com/willwebberley/cenode-alexa&#34; target=&#34;_blank&#34; rel=&#34;noopener noreferrer&#34; &gt;available on GitHub&lt;/a&gt;.&lt;/p&gt;
</content:encoded>
    </item>
    
    <item>
      <title>CENode in IoT</title>
      <link>https://wilw.dev/blog/2017/06/26/cenode-iot/</link>
      <pubDate>Mon, 26 Jun 2017 17:00:00 +0000</pubDate>
      
      <guid>https://wilw.dev/blog/2017/06/26/cenode-iot/</guid>
      
        <category>cenode</category>
      
        <category>iot</category>
      
        <category>hue</category>
      
        <category>project</category>
      
      
      <content:encoded>&lt;p&gt;In a &lt;a href=&#34;https://wilw.dev/blog/2017/06/22/cenode/&#34;&gt;previous note&lt;/a&gt; I discussed CENode and briefly mentioned its potential for use in interacting with the Internet of Things. I thought I’d add a practical example of how it might be used for this and for ’tasking’ other systems.&lt;/p&gt;
&lt;p&gt;I have a few &lt;a href=&#34;http://www2.meethue.com/en-US&#34; target=&#34;_blank&#34; rel=&#34;noopener noreferrer&#34; &gt;Philips Hue&lt;/a&gt; bulbs at home, and the Hue Bridge that enables interaction with the bulbs exposes a nice RESTful API. My aim was to get CENode to use this API to control my lights.&lt;/p&gt;
&lt;p&gt;A working example of the concepts in this note is available &lt;a href=&#34;https://github.com/willwebberley/CENode-IoT&#34; target=&#34;_blank&#34; rel=&#34;noopener noreferrer&#34; &gt;on GitHub&lt;/a&gt; (as a small webapp) and &lt;a href=&#34;https://player.vimeo.com/video/223169323&#34; target=&#34;_blank&#34; rel=&#34;noopener noreferrer&#34; &gt;here’s a short demo video&lt;/a&gt; (which includes a speech-recognition component):&lt;/p&gt;
&lt;p&gt;The first step was to &lt;a href=&#34;https://developers.meethue.com/documentation/configuration-api#71_create_user&#34; target=&#34;_blank&#34; rel=&#34;noopener noreferrer&#34; &gt;generate a username for the Bridge&lt;/a&gt;, which CENode can use to authenticate requests through the API.&lt;/p&gt;
&lt;p&gt;I use &lt;a href=&#34;https://pdfs.semanticscholar.org/d5d5/65fcadcb35579b5ee25cdaa713afa14f7835.pdf&#34; target=&#34;_blank&#34; rel=&#34;noopener noreferrer&#34; &gt;CE cards&lt;/a&gt; to supply instructions to a CENode agent, since this is the generally recognised method for interaction between CE-capable devices. When instantiating a node, any number of CE ‘models’ may be passed in order to form a base knowledge set to work from. Here is such a model for giving CENode a view of the Hue ‘world’:&lt;/p&gt;
&lt;pre tabindex=&#34;0&#34;&gt;&lt;code&gt;const lightModel = [
  &#39;conceptualise a ~ hue bridge ~ h that has the value V as ~ address ~ and has the value W as ~ token ~&#39;,
  &#39;conceptualise a ~ hue bulb ~ h that has the value C as ~ code ~ and has the value V as ~ strength ~&#39;,
  &#39;conceptualise an ~ iot card ~ I that is a card and ~ targets ~ the hue bulb D and has the value P as ~ power ~ and has the value B as ~ brightness ~ and has the value S as ~ saturation ~ and has the value H as ~ hue ~ and has the value C as ~ colour ~&#39;,
  &#39;there is a hue bridge named bridge1 that has \&#39;192.168.1.2\&#39; as address and has \&#39;abc123\&#39; as token&#39;,
];
&lt;/code&gt;&lt;/pre&gt;&lt;p&gt;The model tells the node about Hue Bridges, bulbs, and a new type of card called an &lt;code&gt;iot card&lt;/code&gt;, which supports properties for controlling bulbs. Finally, we instantiate a single bridge with an appropriate IP address and the username/token generated earlier.&lt;/p&gt;
&lt;p&gt;Next the CENode instance needs to be created and its agent prepared:&lt;/p&gt;
&lt;pre tabindex=&#34;0&#34;&gt;&lt;code&gt;const node = new CENode(CEModels.core, lightModel);
const hueBridge = node.concepts.hue_bridge.instances[0];
updateBulbs();
node.attachAgent();
node.agent.setName(&#39;House&#39;);
&lt;/code&gt;&lt;/pre&gt;&lt;p&gt;The &lt;code&gt;updateBulbs()&lt;/code&gt; function (&lt;a href=&#34;https://github.com/willwebberley/CENode-IoT/blob/master/app.js&#34; target=&#34;_blank&#34; rel=&#34;noopener noreferrer&#34; &gt;see it here&lt;/a&gt;) makes a request to the Bridge to download data about known Hue bulbs, which are added to the node’s knowledge base. For example;&lt;/p&gt;
&lt;pre tabindex=&#34;0&#34;&gt;&lt;code&gt;there is a hue bulb named &#39;Lounge&#39; that has &#39;7&#39; as code
&lt;/code&gt;&lt;/pre&gt;&lt;p&gt;The &lt;code&gt;code&lt;/code&gt; property is the unique identifier the bridge uses to determine the bulb on the network.&lt;/p&gt;
&lt;p&gt;Finally, all that was needed was to include a handler function for &lt;code&gt;iot card&lt;/code&gt;s and to add this to the CENode agent:&lt;/p&gt;
&lt;pre tabindex=&#34;0&#34;&gt;&lt;code&gt;node.agent.cardHandler.handlers[&#39;iot card&#39;] = (card) =&gt; {
  if (card.targets){
    const data = {};
    if (card.power) data.on = card.power === &#39;on&#39;;
    if (card.brightness) data.bri = parseInt(card.brightness)
    if (card.saturation) data.sat = parseInt(card.saturation)
    if (card.hue) data.hue = parseInt(card.hue)
    request(&#39;PUT&#39;, hueBridge, &#39;/lights/&#39; &#43; card.targets.code &#43; &#39;/state&#39;, data);
  }
};
&lt;/code&gt;&lt;/pre&gt;&lt;p&gt;The function makes an appropriate request to the Hue Bridge based on the properties of the &lt;code&gt;iot card&lt;/code&gt;. Now, we can submit sentences like this in order to interact with the system (e.g. to turn the ‘Lounge’ bulb on):&lt;/p&gt;
&lt;pre tabindex=&#34;0&#34;&gt;&lt;code&gt;there is an iot card named card1 that is to the agent House and has &#39;instruction&#39; as content and targets the hue bulb &#39;Lounge&#39; and has &#39;on&#39; as power
&lt;/code&gt;&lt;/pre&gt;&lt;p&gt;And that’s it, really. This note contains only the more interesting components of the experiment, but hopefully provides an indication of how the library may be used for simple inter-device communication. The &lt;a href=&#34;https://github.com/willwebberley/CENode-IoT/blob/master/app.js&#34; target=&#34;_blank&#34; rel=&#34;noopener noreferrer&#34; &gt;full demo&lt;/a&gt; includes extra code to handle the UI for a webapp and extra utility functions.&lt;/p&gt;
</content:encoded>
    </item>
    
    <item>
      <title>CENode</title>
      <link>https://wilw.dev/blog/2017/06/22/cenode/</link>
      <pubDate>Thu, 22 Jun 2017 17:00:00 +0000</pubDate>
      
      <guid>https://wilw.dev/blog/2017/06/22/cenode/</guid>
      
        <category>cenode</category>
      
        <category>javascript</category>
      
        <category>cardiffuniversity</category>
      
        <category>ibm</category>
      
        <category>ita</category>
      
        <category>research</category>
      
      
      <content:encoded>&lt;p&gt;Whilst working on the &lt;a href=&#34;http://usukita.com&#34; target=&#34;_blank&#34; rel=&#34;noopener noreferrer&#34; &gt;ITA Project&lt;/a&gt; - a collaborative research programme between the UK MoD and the US Army Research Laboratory -  over the last few years, one of my primary areas has been to research around controlled natural languages, and working with &lt;a href=&#34;http://cf.ac.uk&#34; target=&#34;_blank&#34; rel=&#34;noopener noreferrer&#34; &gt;Cardiff University&lt;/a&gt; and &lt;a href=&#34;https://www.ibm.com/uk-en&#34; target=&#34;_blank&#34; rel=&#34;noopener noreferrer&#34; &gt;IBM UK&lt;/a&gt;’s &lt;a href=&#34;https://emerging-technology.co.uk&#34; target=&#34;_blank&#34; rel=&#34;noopener noreferrer&#34; &gt;Emerging Technology&lt;/a&gt; team to develop CENode.&lt;/p&gt;
&lt;p&gt;As part of the project - before I joined - researchers at IBM developed the &lt;a href=&#34;https://github.com/ce-store/ce-store&#34; target=&#34;_blank&#34; rel=&#34;noopener noreferrer&#34; &gt;CEStore&lt;/a&gt;, which aims to provide tools for working with &lt;a href=&#34;https://developer.ibm.com/open/2016/06/16/ce-store-and-controlled-english-puts-ita-science-library-in-the-spotlight&#34; target=&#34;_blank&#34; rel=&#34;noopener noreferrer&#34; &gt;ITA Controlled English&lt;/a&gt;. Controlled English (CE) is a subset of the English language which is structured in a way that attempts to remove ambiguity from statements, enabling machines to understand ‘English’
inputs.&lt;/p&gt;
&lt;p&gt;Such a language was developed partly to support multi-agent systems consisting of a mixture of humans and machines, and to allow each agent to be able to communicate with one another using the same protocol in coalition scenarios. In these systems, there may be agents on the ground who submit information to the CEStore in CE, which is able to parse and understand the inputs. The CEStore may then pass the information on to other interested parties or may give an agent (such as a drone,
camera, sensor, or other equipment) a task (follow, intersect, watch, etc.) based on the complement of the existing knowledge and the new input.&lt;/p&gt;
&lt;p&gt;An &lt;a href=&#34;https://pdfs.semanticscholar.org/d5d5/65fcadcb35579b5ee25cdaa713afa14f7835.pdf&#34; target=&#34;_blank&#34; rel=&#34;noopener noreferrer&#34; &gt;old example&lt;/a&gt; we use combines the CEStore with a system capable of assigning missions to sensors or equipment (see &lt;a href=&#34;https://users.cs.cf.ac.uk/A.D.Preece/publications/download/spie2012a.pdf&#34; target=&#34;_blank&#34; rel=&#34;noopener noreferrer&#34; &gt;this paper&lt;/a&gt;). This example focuses on ‘John Smith’, who is known to the CE system as a HVT (high-value target) owning a black car with licence plate ‘ABC 123’. A human agent on the ground may later observe a speeding car and issue information into the system through an interface on their mobile device or via a microphone;&lt;/p&gt;
&lt;p&gt;&lt;code&gt;there is a car named car1 which has black as colour and has &#39;ABC 123&#39; as licence plate and is travelling north on North Road&lt;/code&gt;&lt;/p&gt;
&lt;p&gt;The system receiving the message can put together that this speeding car most likely contains John Smith (since it’s known that he owns a car with this licence plate), and so can task a nearby drone to follow it based on the coordinates of the road and the direction of travel.&lt;/p&gt;
&lt;p&gt;A human agent being able to type or speak this precise type of English is unlikely, particularly in emergency or rapid-response scnearios, and so the CEStore has a level of understanding of ’natural’ language, and is able to translate many sentences from natural language English into CE - enabling agents to, largely, speak in a more native fashion.&lt;/p&gt;
&lt;p&gt;The usefulness of the CEStore project led us to consider possibilities of a (lighter) version of a CEStore that could run on mobile devices in a decentralised network of CE-capable devices without relying on a centralised node responsible for parsing and translating all CE inputs. Such a system would also have the benefit of supporting a network of distributed ’nodes’, each with the ability to maintain their own distinct knowledge bases and to understand and ‘speak’ CE - and thus the
concept for CENode was produced.&lt;/p&gt;
&lt;p&gt;A key motivation for this was to support those agents who may not have a consistent network connection to a central server, but who still need knowledge support and the ability to report information - thus building the local knowledge base and improving inferences. Then, once the agent can re-establish a connection to other nodes, new information can propagate through the network.&lt;/p&gt;
&lt;p&gt;The &lt;a href=&#34;http://cenode.io&#34; target=&#34;_blank&#34; rel=&#34;noopener noreferrer&#34; &gt;CENode&lt;/a&gt; project (with &lt;a href=&#34;https://github.com/willwebberley/CENode&#34; target=&#34;_blank&#34; rel=&#34;noopener noreferrer&#34; &gt;source hosted on GitHub&lt;/a&gt;) began with a focus on supporting our &lt;a href=&#34;http://ieeexplore.ieee.org/abstract/document/7936494&#34; target=&#34;_blank&#34; rel=&#34;noopener noreferrer&#34; &gt;SHERLOCK experiments&lt;/a&gt;, which had traditionally been powered using the CEStore. Using CENode, users of SHERLOCK experienced benefits such as auto-correct and typing suggestions, the ability to continue working offline (with information syncing when a network is re-established), and the display of a personalised ‘dashboard’ indicating the local agent’s view of the world represented by the game.&lt;/p&gt;
&lt;p&gt;The SHERLOCK experiment was even &lt;a href=&#34;http://www.bbc.co.uk/news/technology-34423291&#34; target=&#34;_blank&#34; rel=&#34;noopener noreferrer&#34; &gt;covered by the BBC&lt;/a&gt;.&lt;/p&gt;
&lt;p&gt;Since then, the CENode project has grown, and many of the features enjoyed by the CEStore (which is written in Java and deployed using Apache Tomcat) have been re-implemented for CENode. The library supports rules that fire given specific inputs, simple natural language understanding and parsing, querying through CE inputs, the CE cards &lt;a href=&#34;https://pdfs.semanticscholar.org/d5d5/65fcadcb35579b5ee25cdaa713afa14f7835.pdf&#34; target=&#34;_blank&#34; rel=&#34;noopener noreferrer&#34; &gt;blackboard architecture&lt;/a&gt;, and policies - enabling CENode instances to communicate with each other in different topologies.&lt;/p&gt;
&lt;p&gt;CENode is written in JavaScript, since this allows it to be downloaded to and cached on any JavaScript-supporting browser (for example, on a mobile phone or tablet), and to run as a Node app.&lt;/p&gt;
&lt;p&gt;In addition to using the CE-based (‘cards’) interfaces, CENode can be interacted-with using the JavaScript bindings and can expose RESTful APIs when run as a Node app, enabling several types of CENode deployments to work together as part of a single system.&lt;/p&gt;
&lt;p&gt;Check out a demo of the library &lt;a href=&#34;http://cenode.io/demo/index.html&#34; target=&#34;_blank&#34; rel=&#34;noopener noreferrer&#34; &gt;here&lt;/a&gt;, which wraps a simple user interface around the library’s JavaScript bindings. In the demo, the local CENode agent is preloaded with some knowledge about planets and stars. Try asking it questions or teaching it something new. Additionally, we have deployed a service called &lt;a href=&#34;http://explorer.cenode.io&#34; target=&#34;_blank&#34; rel=&#34;noopener noreferrer&#34; &gt;CENode Explorer&lt;/a&gt; which can launch cloud-based CENode instances and allows you to browse the knowledge base.&lt;/p&gt;
&lt;p&gt;We hope to continue to maintain CENode as part of the project, and to discover more interesting use-cases. There are already clear pathways for its use in voice assistants, bots,  and as a protocol for communication in IoT devices (some work for which is already underway). Those interested in developing with the library can get started using &lt;a href=&#34;https://github.com/willwebberley/CENode/wiki&#34; target=&#34;_blank&#34; rel=&#34;noopener noreferrer&#34; &gt;the CENode Wiki&lt;/a&gt;.&lt;/p&gt;
</content:encoded>
    </item>
    
    <item>
      <title>Two Year Update</title>
      <link>https://wilw.dev/blog/2017/03/16/two-year-update/</link>
      <pubDate>Thu, 16 Mar 2017 17:00:00 +0000</pubDate>
      
      <guid>https://wilw.dev/blog/2017/03/16/two-year-update/</guid>
      
        <category>travel</category>
      
        <category>life</category>
      
      
      <content:encoded>&lt;p&gt;I haven’t written a post since summer 2015. It’s now March 2017 and I thought I’d write an update very briefly covering the last couple of years.&lt;/p&gt;
&lt;p&gt;I finished researching and lecturing full-time in the summer of 2015. It felt like the end of an era; I’d spent around a third of my life at the &lt;a href=&#34;http://www.cardiff.ac.uk/computer-science&#34; target=&#34;_blank&#34; rel=&#34;noopener noreferrer&#34; &gt;School of Computer Science and Informatics&lt;/a&gt; at &lt;a href=&#34;http://cf.ac.uk&#34; target=&#34;_blank&#34; rel=&#34;noopener noreferrer&#34; &gt;Cardiff University&lt;/a&gt;, and had experienced time there as an undergraduate through to postgrad and on to full-time staff. However, I felt it was time to move on and to try something new, although I was really pleased to be able to continue working with them on a more casual part-time basis - something that continues to today.&lt;/p&gt;
&lt;p&gt;In that summer after leaving full-time work at Cardiff I went &lt;a href=&#34;http://www.interrail.eu&#34; target=&#34;_blank&#34; rel=&#34;noopener noreferrer&#34; &gt;interailing&lt;/a&gt; around Europe with my friend, Dan. It was an amazing experience through which I had a taste of many new European cities where we met lots of interesting people. We started by flying out to Berlin, and from there our route took us through Prague, Krakow, Budapest, Bratislava, Vienna, Munich, Koblenz, Luxembourg City, Brussels, Antwerp, and then finished in Amsterdam (which I’d been to before, but always love visiting).&lt;/p&gt;
&lt;p&gt;&lt;img src=&#34;https://wilw.dev/media/blog/interrailing.png&#34; alt=&#34;Interailing&#34;&gt;&lt;/p&gt;
&lt;p&gt;&lt;em&gt;Some photos from the Interrail trip.&lt;/em&gt;&lt;/p&gt;
&lt;p&gt;After returning, I moved to London to start a new full-time job with &lt;a href=&#34;https://www.chaser.io&#34; target=&#34;_blank&#34; rel=&#34;noopener noreferrer&#34; &gt;Chaser&lt;/a&gt; Having met the founders David and Mark at a previous &lt;a href=&#34;https://www.siliconmilkroundabout.com&#34; target=&#34;_blank&#34; rel=&#34;noopener noreferrer&#34; &gt;Silicon Milkroundabout&lt;/a&gt;, Chaser was so great to get involved with - I was part of a fab team creating fin-tech software with a goal to help boost the cashflows in small-medium sized businesses. Working right in the City was fun and totally different to what seemed like a much quieter life in Cardiff. Whilst there, I learned loads more about web-based programming and was able to put some of the data-analysis skills from my PhD to use.&lt;/p&gt;
&lt;p&gt;At the end of 2015 I was to move back to South Wales to begin a new job at &lt;a href=&#34;https://simplydo.co.uk&#34; target=&#34;_blank&#34; rel=&#34;noopener noreferrer&#34; &gt;Simply Do Ideas&lt;/a&gt; as a senior engineer. Again, this was a totally different experience involving a shift from fin-tech to ed-tech and a move from the relentless busy-ness of London to the quieter (but no less fun) life of Caerphilly - where our offices were based. Since I was to head the technical side of the business, I was able to put my own stamp on the company and the product, and was able to help decide its future and direction.&lt;/p&gt;
&lt;p&gt;&lt;img src=&#34;https://wilw.dev/media/blog/sdi_bett.jpg&#34; alt=&#34;Simply Do team&#34;&gt;&lt;/p&gt;
&lt;p&gt;&lt;em&gt;Myself and Josh representing Simply Do Ideas at Bett 2017 in London.&lt;/em&gt;&lt;/p&gt;
&lt;p&gt;In February 2016 I was honoured to be promoted to the Simply Do Ideas board and to have been made the company’s Chief Technology Officer. Over the last year myself and the rest of the team have been proud to be part of a company growing very highly respected in a really interesting and exciting domain, and we’re all very  excited about what’s to come in the near (and far) future!&lt;/p&gt;
&lt;p&gt;I still continue to work with Cardiff University on some research projects and to help out with some of the final-year students there, I hope to write a little more about this work soon.&lt;/p&gt;
&lt;p&gt;I feel so lucky to have been able to experience so much in such a short time frame - from academic research and teaching, being a key part of two growth startups, heading a tech company’s technology arm, being a member of a board along with very highly-respected and successful entrepreneurs and business owners, and getting to meet such a wide range of great people. I feel like I’ve grown and learned so much - both professionally and personally - from all of my experiences and from everyone I’ve met along the way.&lt;/p&gt;
</content:encoded>
    </item>
    
    <item>
      <title>Android: Consuming Nintendo Hotspot Data</title>
      <link>https://wilw.dev/blog/2015/05/27/android-consuming-nintendo-hotspot-data/</link>
      <pubDate>Wed, 27 May 2015 17:00:00 +0000</pubDate>
      
      <guid>https://wilw.dev/blog/2015/05/27/android-consuming-nintendo-hotspot-data/</guid>
      
        <category>android</category>
      
        <category>nintendo</category>
      
        <category>project</category>
      
        <category>technology</category>
      
      
      <content:encoded>&lt;p&gt;I recently &lt;a href=&#34;https://wilw.dev/blog/2015/5/12/nintendos-hotspot-api&#34;&gt;blogged about&lt;/a&gt; Nintendo Hotspot data and mentioned it could be more usefully consumable in a native mobile app.&lt;/p&gt;
&lt;p&gt;&lt;img src=&#34;https://wilw.dev/media/blog/android-hotspot.png&#34; alt=&#34;Android Hotspot&#34;&gt;&lt;/p&gt;
&lt;p&gt;As such, I wrote a small Android app for retrieving this data and displaying it on a Google Map. The app shows nearby hotspots, allows users to also search for other non-local places, and shows information on the venue hosting the zone.&lt;/p&gt;
&lt;p&gt;The app is available on the &lt;a href=&#34;https://play.google.com/store/apps/details?id=net.flyingsparx.spotpassandroid&#34; target=&#34;_blank&#34; rel=&#34;noopener noreferrer&#34; &gt;Play Store&lt;/a&gt; and its source is published on &lt;a href=&#34;https://github.com/willwebberley/NZone-finder&#34; target=&#34;_blank&#34; rel=&#34;noopener noreferrer&#34; &gt;GitHub&lt;/a&gt;.&lt;/p&gt;
</content:encoded>
    </item>
    
    <item>
      <title>Nintendo&#39;s Hotspot &#39;API&#39;</title>
      <link>https://wilw.dev/blog/2015/05/12/nintendos-hotspot-api/</link>
      <pubDate>Tue, 12 May 2015 17:00:00 +0000</pubDate>
      
      <guid>https://wilw.dev/blog/2015/05/12/nintendos-hotspot-api/</guid>
      
        <category>android</category>
      
        <category>nintendo</category>
      
        <category>technology</category>
      
      
      <content:encoded>&lt;p&gt;Since getting a DS, &lt;a href=&#34;http://www.nintendo.com/3ds/built-in-software/streetpass&#34; target=&#34;_blank&#34; rel=&#34;noopener noreferrer&#34; &gt;StreetPass&lt;/a&gt; has become quite addictive. It’s actually pretty fun checking the device after walking through town or using public transport to see a list of Miis representing the people you’ve been near recently, and the minigames (such as StreetPass Quest) that require you to ‘meet’ people in order to advance also make it more involved. Essentially the more you’re out and about, the further you can progress - this is further accentuated through Play Coins, which can be used to help ‘buy’ your way forward and are earned for every 100 steps taken whilst holding the device.&lt;/p&gt;
&lt;p&gt;&lt;img src=&#34;https://wilw.dev/media/blog/nintendozone2.png&#34; alt=&#34;Nintendo Zone&#34;&gt;&lt;/p&gt;
&lt;p&gt;The DS systems can also use relay points in Nintendo Zone hotspots to collect StreetPass hits. These zones are special WiFi access points hosted in certain commercial venues (e.g. in McDonalds and Subway restaurants), and allow you to ‘meet’ people around the world who also happen to be in another Nintendo Zone at the same time. As such, users can get a lot of hits very quickly (up to a maximum of 10 at a time). There are various ways people have &lt;a href=&#34;https://gbatemp.net/threads/how-to-have-a-homemade-streetpass-relay.352645&#34; target=&#34;_blank&#34; rel=&#34;noopener noreferrer&#34; &gt;found&lt;/a&gt; to set up a ‘home’ zone, but Nintendo have also published a &lt;a href=&#34;https://microsite.nintendo-europe.com/hotspots&#34; target=&#34;_blank&#34; rel=&#34;noopener noreferrer&#34; &gt;map&lt;/a&gt; to display official nearby zones.&lt;/p&gt;
&lt;p&gt;However, their map seems a little clunky to use while out and about, so I wanted to see if there could be an easier way to get this information more quickly. When using the map, the network logs revealed &lt;code&gt;GET&lt;/code&gt; requests being made to:&lt;/p&gt;
&lt;pre tabindex=&#34;0&#34;&gt;&lt;code&gt;https://microsite.nintendo-europe.com/hotspots/api/hotspots/get
&lt;/code&gt;&lt;/pre&gt;&lt;p&gt;The location for which to retrieve data is specified through the &lt;code&gt;zoom&lt;/code&gt; and &lt;code&gt;bbox&lt;/code&gt; parameters, which seem to map directly to the zoom level and the bounds reported by the underlying Google Maps API being used. For some reason, the parameter &lt;code&gt;ummary_mode=true&lt;/code&gt; also needs to be set. As such, a (unencoded) request for central Cardiff may look like this:&lt;/p&gt;
&lt;pre tabindex=&#34;0&#34;&gt;&lt;code&gt;/hotspots/api/hotspots/get?summary_mode=true&amp;zoom=18&amp;bbox=51.480043,-3.180592,51.483073,-3.173028
&lt;/code&gt;&lt;/pre&gt;&lt;p&gt;Where the coordinates (&lt;code&gt;51.480043,-3.180592&lt;/code&gt; and (&lt;code&gt;51.483073,-3.173028&lt;&lt;/code&gt;) respectively represent the lower-left and upper-right corners of the bounding box. The response is in JSON, and contains a lat/lng for each zone, a name, and an ID that can be used to retrieve more information about the host’s zone using this URL format:&lt;/p&gt;
&lt;pre tabindex=&#34;0&#34;&gt;&lt;code&gt;https://microsite.nintendo-europe.com/hotspots/#hotspot/&amp;lt;ID&amp;gt;
&lt;/code&gt;&lt;/pre&gt;&lt;p&gt;When the map is zoomed-out (to prevent map-cluttering) a zone ‘group’ might be returned instead of an individual zone, for each of which the size is indicated. Zooming back in to a group then reveals the individual zones existing in that area.&lt;/p&gt;
&lt;p&gt;&lt;img src=&#34;https://wilw.dev/media/blog/nintendozone1.png&#34; alt=&#34;Nintendo Zone 2&#34;&gt;&lt;/p&gt;
&lt;p&gt;It seems that this server endpoint does not support cross-origin resource-sharing (CORS), which means that the data is not retrievable for a third-party web-app (at least, without some degree of proxying) due to browser restrictions. However, and especially since the endpoint currently requires no session implementation or other kind of authentication, the data seems very easily retrievable and manageable for non-browser applications and other types of systems.&lt;/p&gt;
</content:encoded>
    </item>
    
    <item>
      <title>Using Weka in Go</title>
      <link>https://wilw.dev/blog/2015/05/01/using-weka-in-go/</link>
      <pubDate>Fri, 01 May 2015 17:00:00 +0000</pubDate>
      
      <guid>https://wilw.dev/blog/2015/05/01/using-weka-in-go/</guid>
      
        <category>weka</category>
      
        <category>golang</category>
      
        <category>machinelearning</category>
      
        <category>technology</category>
      
      
      <content:encoded>&lt;p&gt;A couple of years ago I wrote a &lt;a href=&#34;https://wilw.dev/blog/13/6/12/wekapy&#34;&gt;blog post&lt;/a&gt; about wrapping some of &lt;a href=&#34;http://www.cs.waikato.ac.nz/ml/weka&#34; target=&#34;_blank&#34; rel=&#34;noopener noreferrer&#34; &gt;Weka&lt;/a&gt;’s classification functionality to allow it to be used programmatically in Python programs. A small project I’m currently working on at home is around taking some of the later research from my PhD work to see if it can be expressed and used as a simple web-app.&lt;/p&gt;
&lt;p&gt;I began development in &lt;a href=&#34;https://golang.org&#34; target=&#34;_blank&#34; rel=&#34;noopener noreferrer&#34; &gt;Go&lt;/a&gt; as I hadn’t yet spent much time working with the language. The research work involves using a Bayesian network classifier to help infer a &lt;a href=&#34;http://ieeexplore.ieee.org/xpls/abs_all.jsp?arnumber=6686092&amp;tag=1&#34; target=&#34;_blank&#34; rel=&#34;noopener noreferrer&#34; &gt;tweet’s interestingness&lt;/a&gt;, and while Go machine-learning toolkits do &lt;a href=&#34;http://biosphere.cc/software-engineering/go-machine-learning-nlp-libraries&#34; target=&#34;_blank&#34; rel=&#34;noopener noreferrer&#34; &gt;exist&lt;/a&gt;, I wanted to use my existing models that were serialized in Java by Weka.&lt;/p&gt;
&lt;p&gt;I started working on &lt;a href=&#34;https://github.com/willwebberley/WekaGo&#34; target=&#34;_blank&#34; rel=&#34;noopener noreferrer&#34; &gt;WekaGo&lt;/a&gt;, which is able to programmatically support simple classification tasks within a Go program. It essentially just manages the model, abstracts the generation of &lt;a href=&#34;http://www.cs.waikato.ac.nz/ml/weka/arff.html&#34; target=&#34;_blank&#34; rel=&#34;noopener noreferrer&#34; &gt;ARFF&lt;/a&gt; files, and executes the necessary Java to make it quick and easy to train and classify data:&lt;/p&gt;
&lt;pre tabindex=&#34;0&#34;&gt;&lt;code&gt;model := wekago.NewModel(&#34;bayes.BayesNet&#34;)
...
model.AddTrainingInstance(train_instance1)
...
model.Train()
model.AddTestingInstance(train_instance1)
...
model.Test()
&lt;/code&gt;&lt;/pre&gt;&lt;p&gt;Results from the classification can then be examined, as &lt;a href=&#34;https://github.com/willwebberley/WekaGo/blob/master/README.md&#34; target=&#34;_blank&#34; rel=&#34;noopener noreferrer&#34; &gt;described&lt;/a&gt;.&lt;/p&gt;
</content:encoded>
    </item>
    
    <item>
      <title>Media and volume keys in i3</title>
      <link>https://wilw.dev/blog/2015/04/28/media-and-volume-keys-in-i3/</link>
      <pubDate>Tue, 28 Apr 2015 17:00:00 +0000</pubDate>
      
      <guid>https://wilw.dev/blog/2015/04/28/media-and-volume-keys-in-i3/</guid>
      
        <category>linux</category>
      
        <category>i3</category>
      
        <category>technology</category>
      
      
      <content:encoded>&lt;p&gt;As is the case with many people, all music I listen to on my PC these days plays from the web through a browser. I’m a heavy user of Google Play Music and SoundCloud, and using Chrome to handle everything means playlists and libraries (and the way I use them through extensions) sync up properly everywhere I need them.&lt;/p&gt;
&lt;p&gt;On OS X I use &lt;a href=&#34;http://beardedspice.com&#34; target=&#34;_blank&#34; rel=&#34;noopener noreferrer&#34; &gt;BearededSpice&lt;/a&gt; to map the keyboard media controls to browser-based music-players, and the volume keys adjusted the system as they should. Using &lt;a href=&#34;https://i3wm.org&#34; target=&#34;_blank&#34; rel=&#34;noopener noreferrer&#34; &gt;i3&lt;/a&gt; (and other lightweight window managers) can make you realise what you take for granted when using more fully-fledged arrangements, but it doesn’t take long to achieve the same functionality on such systems.&lt;/p&gt;
&lt;p&gt;A quick search revealed &lt;a href=&#34;https://github.com/borismus/keysocket&#34; target=&#34;_blank&#34; rel=&#34;noopener noreferrer&#34; &gt;keysocket&lt;/a&gt; - a Chrome extension that listens out for the hardware media keys and is able to interact with a large list of supported music websites. In order to get the volume controls working, I needed to map i3 through to &lt;code&gt;alsa&lt;/code&gt;, and this turned out to be pretty straight-forward too. It only required the addition of three lines to my i3 config to handle the volume-up, volume-down, and mute keys:&lt;/p&gt;
&lt;pre tabindex=&#34;0&#34;&gt;&lt;code&gt;bindsym XF86AudioRaiseVolume exec amixer -q set Master 4%&#43; unmute
bindsym XF86AudioLowerVolume exec amixer -q set Master 4%- unmute
bindsym XF86AudioMute exec amixer -q set Master toggle
&lt;/code&gt;&lt;/pre&gt;&lt;p&gt;And for fun added the block below to &lt;code&gt;~/.i3status.conf&lt;/code&gt; to get the volume displayed on the status bar:&lt;/p&gt;
&lt;pre tabindex=&#34;0&#34;&gt;&lt;code&gt;volume master {
    format = &#34;♪ %volume &#34;
    device = &#34;default&#34;
    mixer = &#34;Master&#34;
    mixer_idx = 0
}
&lt;/code&gt;&lt;/pre&gt;</content:encoded>
    </item>
    
    <item>
      <title>Web and Social Computing</title>
      <link>https://wilw.dev/blog/2015/02/18/web-and-social-computing/</link>
      <pubDate>Wed, 18 Feb 2015 17:00:00 +0000</pubDate>
      
      <guid>https://wilw.dev/blog/2015/02/18/web-and-social-computing/</guid>
      
        <category>cardiffuniversity</category>
      
        <category>teaching</category>
      
      
      <content:encoded>&lt;p&gt;his week I begin lecturing a module for &lt;a href=&#34;http://cs.cf.ac.uk&#34; target=&#34;_blank&#34; rel=&#34;noopener noreferrer&#34; &gt;Cardiff School of Computer Science and Informatics&lt;/a&gt;’ postgraduate MSc course in &lt;a href=&#34;http://courses.cardiff.ac.uk/postgraduate/course/detail/p071.html&#34; target=&#34;_blank&#34; rel=&#34;noopener noreferrer&#34; &gt;Advanced Computer Science&lt;/a&gt;.&lt;/p&gt;
&lt;p&gt;The module is called Web and Social Computing, with the main aim being to introduce students to the concepts of social computing and web-based systems. The course will include both theory and practical sessions in order to allow them to enhance their knowledge derived from literature with the practice of key concepts. We’ll also have lots of guest lectures from experts in specific areas to help reinforce the importance of this domain.&lt;/p&gt;
&lt;p&gt;As part of the module, I will encourage students to try and increase their web-presence and to interact with a wider community on the Internet. They’ll do this by engaging more with social media and by maintaining a blog on things they’ve learned and researched.&lt;/p&gt;
&lt;p&gt;Each week, the students will give a 5-minute &lt;a href=&#34;http://en.wikipedia.org/wiki/Ignite_%28event%29&#34; target=&#34;_blank&#34; rel=&#34;noopener noreferrer&#34; &gt;Ignite-format&lt;/a&gt; talk on the research they’ve carried out. The quick presentation style will allow everyone in the group to convey what they feel are the most important and relevant parts in current research across many of the topics covered in the module.&lt;/p&gt;
&lt;p&gt;We’ll cover quite a diverse range of topics, starting from an introduction to networks and a coverage of mathematical graph theory. This will lead on to social networks, including using APIs to harvest data in useful ways. Over the last few weeks, we’ll delve into subjects around socially-driven business models and peer-to-peer finance systems, such as BitCoin.&lt;/p&gt;
&lt;p&gt;During the course, I hope that students will gain practical experience with various technologies, such as &lt;a href=&#34;https://networkx.github.io&#34; target=&#34;_blank&#34; rel=&#34;noopener noreferrer&#34; &gt;NetworkX&lt;/a&gt; for modelling and visualising graphs in Python, &lt;a href=&#34;http://www.cs.waikato.ac.nz/ml/weka&#34; target=&#34;_blank&#34; rel=&#34;noopener noreferrer&#34; &gt;Weka&lt;/a&gt; for some machine learning and classification, and good practices for building and using web APIs.&lt;/p&gt;
</content:encoded>
    </item>
    
    <item>
      <title>Developing Useful APIs for the Web</title>
      <link>https://wilw.dev/blog/2015/02/05/developing-useful-apis-for-the-web/</link>
      <pubDate>Thu, 05 Feb 2015 17:00:00 +0000</pubDate>
      
      <guid>https://wilw.dev/blog/2015/02/05/developing-useful-apis-for-the-web/</guid>
      
        <category>talk</category>
      
        <category>webapi</category>
      
      
      <content:encoded>&lt;p&gt;Yesterday, I gave a talk about my experiences with developing and using RESTful APIs, with the goal of providing tips for structuring such interfaces so that they work in a useful and sensible way.&lt;/p&gt;
&lt;p&gt;I went back to first principles, with overviews of basic HTTP messages as part of the request-response cycle and using sensible status codes in HTTP responses. I discussed the benefits of ‘collection-oriented’ endpoint URLs to identify resources that can be accessed and modified and the use of HTTP methods to describe what to do with these resources.&lt;/p&gt;
</content:encoded>
    </item>
    
    <item>
      <title>NHS Hack Day</title>
      <link>https://wilw.dev/blog/2015/01/27/nhs-hack-day/</link>
      <pubDate>Tue, 27 Jan 2015 17:00:00 +0000</pubDate>
      
      <guid>https://wilw.dev/blog/2015/01/27/nhs-hack-day/</guid>
      
        <category>event</category>
      
        <category>nhs</category>
      
      
      <content:encoded>&lt;p&gt;This weekend I took part in the &lt;a href=&#34;http://nhshackday.com&#34; target=&#34;_blank&#34; rel=&#34;noopener noreferrer&#34; &gt;NHS Hack Day&lt;/a&gt;. The idea of the event is to bring healthcare professionals together with technology enthusiasts in order to build stuff that is useful for those within the NHS and for those that use it. It was organised by &lt;a href=&#34;https://twitter.com/amcunningham%22&#34; target=&#34;_blank&#34; rel=&#34;noopener noreferrer&#34; &gt;AnneMarie Cunningham&lt;/a&gt;, who did a great job in making the whole thing run smoothly!&lt;/p&gt;
&lt;p&gt;&lt;img src=&#34;https://wilw.dev/media/blog/nhshackday2.jpg&#34; alt=&#34;NHS Hack Day&#34;&gt;&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;This was our team! The image is released under a Creative Commons BY-NC2.0 license by &lt;a href=&#34;https://www.flickr.com/photos/paul_clarke&#34; target=&#34;_blank&#34; rel=&#34;noopener noreferrer&#34; &gt;Paul Clarke&lt;/a&gt;.&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;I was asked to go along and give a hand by &lt;a href=&#34;http://martinjc.com&#34; target=&#34;_blank&#34; rel=&#34;noopener noreferrer&#34; &gt;Martin&lt;/a&gt;, who also had four of his MSc students with him. &lt;a href=&#34;http://mattjw.net&#34; target=&#34;_blank&#34; rel=&#34;noopener noreferrer&#34; &gt;Matt&lt;/a&gt;, previously from &lt;a href=&#34;http://cs.cf.ac.uk&#34; target=&#34;_blank&#34; rel=&#34;noopener noreferrer&#34; &gt;Cardiff CS&amp;I&lt;/a&gt;, also came to provide his data-handling expertise.&lt;/p&gt;
&lt;p&gt;&lt;img src=&#34;https://wilw.dev/media/blog/nhshackday.png&#34; alt=&#34;NHS Hack Day 2&#34;&gt;&lt;/p&gt;
&lt;p&gt;We built a webapp, called &lt;a href=&#34;http://compjcdf.github.io/nhs_hack/app.html&#34; target=&#34;_blank&#34; rel=&#34;noopener noreferrer&#34; &gt;Health Explorer Wales&lt;/a&gt;, that attempts to visualise various data for health boards and communities in Wales. One of the main goals of the app was to make it maintainable, so that users in future could easily add their own geographic or numeric data to visualise. For this, it was important to decide on an extensible &lt;a href=&#34;https://github.com/CompJCDF/nhs_hack/blob/master/data/descriptors.json&#34; target=&#34;_blank&#34; rel=&#34;noopener noreferrer&#34; &gt;data schema&lt;/a&gt; for describing data, and suitable data formats.&lt;/p&gt;
&lt;p&gt;Once the schema was finalised, we were able to go ahead and build the front-end, which used &lt;a href=&#34;http://d3js.org&#34; target=&#34;_blank&#34; rel=&#34;noopener noreferrer&#34; &gt;D3.js&lt;/a&gt; to handle the visualisations. This was the only third-party library we used in the end. The rest of the interface included controls, such as a dataset-selector and controls for sliding back through time (for timeseries data). The app is purely front-end, which means it can essentially be shipped as a single HTML file (with linked scripts and styles).&lt;/p&gt;
&lt;p&gt;We also included an ‘add dataset’ feature, which allows users to add a dataset to be visualised, as long as the schema is observed. In true hackathon style, any exceptions thrown will currently cause the process to fail silently ;) The &lt;a href=&#34;https://github.com/CompJCDF/nhs_hack&#34; target=&#34;_blank&#34; rel=&#34;noopener noreferrer&#34; &gt;GitHub repository&lt;/a&gt; for the app contains a wiki with some guidance on data-formatting. Since the app is front-end only, any data added is persisted using HTML5 local storage and is therefore user-specific.&lt;/p&gt;
&lt;p&gt;Generally, I am pleased with the result. The proof-of-concept is (mostly) mobile-friendly, and allows for easily showing off data in a more comprehensible way than through just using spreadsheets. Although we focussed on visualising only two datatypes initially (we all &lt;3 &lt;a href=&#34;https://twitter.com/_r_309&#34; target=&#34;_blank&#34; rel=&#34;noopener noreferrer&#34; &gt;#maps&lt;/a&gt;), we hope to extend this by dropping in modules for supporting new formats in the future.&lt;/p&gt;
&lt;p&gt;There were many successful projects completed as part of the event, including a new ’eye-test’ concept involving a zombie game using an Oculus Rift and an app for organising group coastal walks around Wales. A full list of projects is available on the event’s &lt;a href=&#34;http://nhshackday.com/previous/events/2015/01/cardiff%22&#34; target=&#34;_blank&#34; rel=&#34;noopener noreferrer&#34; &gt;website&lt;/a&gt;. I really enjoyed the weekend and hope to make the next one in London in May!&lt;/p&gt;
</content:encoded>
    </item>
    
    <item>
      <title>End of an Era</title>
      <link>https://wilw.dev/blog/2015/01/20/end-of-an-era/</link>
      <pubDate>Tue, 20 Jan 2015 17:00:00 +0000</pubDate>
      
      <guid>https://wilw.dev/blog/2015/01/20/end-of-an-era/</guid>
      
        <category>life</category>
      
        <category>phd</category>
      
        <category>research</category>
      
      
      <content:encoded>&lt;p&gt;I recently received confirmation of my completed PhD! I submitted my thesis in May 2014, passed my viva in September and returned my final corrections in December.&lt;/p&gt;
&lt;p&gt;I was examined internally by &lt;a href=&#34;http://burnap.org&#34; target=&#34;_blank&#34; rel=&#34;noopener noreferrer&#34; &gt;Dr Pete Burnap&lt;/a&gt; and also by &lt;a href=&#34;http://www.iis.ee.ic.ac.uk/~j.pitt/Home.html&#34; target=&#34;_blank&#34; rel=&#34;noopener noreferrer&#34; &gt;Dr Jeremy Pitt&lt;/a&gt; of Imperial College London.&lt;/p&gt;
&lt;p&gt;The whole PhD was an amazing experience, even during the more stressful moments. I learnt a huge amount across many domains and I cannot thank my supervisors, &lt;a href=&#34;http://users.cs.cf.ac.uk/Stuart.M.Allen&#34; target=&#34;_blank&#34; rel=&#34;noopener noreferrer&#34; &gt;Dr Stuart Allen&lt;/a&gt; and &lt;a href=&#34;http://users.cs.cf.ac.uk/R.M.Whitaker&#34; target=&#34;_blank&#34; rel=&#34;noopener noreferrer&#34; &gt;Prof Roger Whitker&lt;/a&gt;, enough for their fantastic support and guidance throughout.&lt;/p&gt;
</content:encoded>
    </item>
    
    <item>
      <title>Talk on Open-Source Contribution</title>
      <link>https://wilw.dev/blog/2014/03/26/talk-on-open-source-contribution/</link>
      <pubDate>Wed, 26 Mar 2014 17:00:00 +0000</pubDate>
      
      <guid>https://wilw.dev/blog/2014/03/26/talk-on-open-source-contribution/</guid>
      
        <category>talk</category>
      
        <category>opensource</category>
      
      
      <content:encoded>&lt;p&gt;Today I gave an internal talk at the School of Computer Science &amp; Informatics about open-source contribution.&lt;/p&gt;
&lt;p&gt;The talk  described some of the disadvantages of the ways in which hobbyists and the non-professional sector publicly publish their code. A lot of the time these projects do not receive much visibility or use from others.&lt;/p&gt;
&lt;p&gt;Public contribution is important to the open-source community, which is driven largely by volunteers and enthusiasts, so the point of the talk was to try and encourage people to share expert knowledge through contributing documentation (wikis, forums, articles, etc.), maintaining and adopting packages, and getting more widely involved.&lt;/p&gt;
</content:encoded>
    </item>
    
    <item>
      <title>Node.js Contribution to Heroku&#39;s Dev Center</title>
      <link>https://wilw.dev/blog/2014/03/17/node.js-contribution-to-herokus-dev-center/</link>
      <pubDate>Mon, 17 Mar 2014 17:00:00 +0000</pubDate>
      
      <guid>https://wilw.dev/blog/2014/03/17/node.js-contribution-to-herokus-dev-center/</guid>
      
        <category>contribution</category>
      
        <category>heroku</category>
      
        <category>javascript</category>
      
      
      <content:encoded>&lt;p&gt;I recently wrote a new article for Heroku’s Dev Center on carrying out asynchronous direct-to-S3 uploads using Node.js.&lt;/p&gt;
&lt;p&gt;he article is based heavily on the previous &lt;a href=&#34;https://wilw.dev/blog/13/5/7/contribution-to-heroku-dev-center/&#34;&gt;Python version&lt;/a&gt;, where the only major change is the method for signing the AWS request. This method was outlined in an &lt;a href=&#34;https://wilw.dev/blog/2014/1/17/direct-to-s3-uploads-in-node.js&#34;&gt;earlier blog post&lt;/a&gt;.&lt;/p&gt;
&lt;p&gt;The article is available &lt;a href=&#34;https://devcenter.heroku.com/articles/s3-upload-node&#34; target=&#34;_blank&#34; rel=&#34;noopener noreferrer&#34; &gt;here&lt;/a&gt; and there is also a &lt;a href=&#34;https://github.com/willwebberley/NodeDirectUploader&#34; target=&#34;_blank&#34; rel=&#34;noopener noreferrer&#34; &gt;companion code repository&lt;/a&gt; for the example it describes.&lt;/p&gt;
</content:encoded>
    </item>
    
    <item>
      <title>Seminar at King&#39;s College London</title>
      <link>https://wilw.dev/blog/2014/01/28/seminar-at-kings-college-london/</link>
      <pubDate>Tue, 28 Jan 2014 17:00:00 +0000</pubDate>
      
      <guid>https://wilw.dev/blog/2014/01/28/seminar-at-kings-college-london/</guid>
      
        <category>talk</category>
      
        <category>kcl</category>
      
        <category>research</category>
      
      
      <content:encoded>&lt;p&gt;Last week, I was invited to give a seminar to the Agents and Intelligent Systems group in the &lt;a href=&#34;http://www.kcl.ac.uk/nms/depts/informatics/index.aspx&#34; target=&#34;_blank&#34; rel=&#34;noopener noreferrer&#34; &gt;Department of Informatics&lt;/a&gt; at King’s College London.&lt;/p&gt;
&lt;p&gt;I gave an overview of my PhD research conducted over the past two or three years, from my initial research into retweet behaviours and propagation characteristics through to studies on the properties exhibited by Twitter’s social graph and the effects that the interconnection of users have on message dissemination.&lt;/p&gt;
&lt;p&gt;I finished by outlining our methods for identifying interesting content on Twitter and by demonstrating its relative strengths and weaknesses as were made clear by crowd-sourced validations carried out on the methodology results.&lt;/p&gt;
&lt;p&gt;There was some very interesting and useful questions from the audience, some of which is now being taken into consideration in my thesis. It was also good to visit another computer science department and to hear about the work done independently and collaboratively by its different research groups.&lt;/p&gt;
&lt;p&gt;The slides from the seminar are available &lt;a href=&#34;http://flyingsparx.net/static/downloads/kcl_seminar_2014.pdf&#34; target=&#34;_blank&#34; rel=&#34;noopener noreferrer&#34; &gt;here&lt;/a&gt; and there is a &lt;a href=&#34;http://inkings.org/2014/02/03/tweets-and-retweets&#34; target=&#34;_blank&#34; rel=&#34;noopener noreferrer&#34; &gt;blog post&lt;/a&gt; about it on the Department of Informatics’ website.&lt;/p&gt;
</content:encoded>
    </item>
    
    <item>
      <title>Direct-to-S3 Uploads in Node.js</title>
      <link>https://wilw.dev/blog/2014/01/17/direct-to-s3-uploads-in-node.js/</link>
      <pubDate>Fri, 17 Jan 2014 17:00:00 +0000</pubDate>
      
      <guid>https://wilw.dev/blog/2014/01/17/direct-to-s3-uploads-in-node.js/</guid>
      
        <category>heroku</category>
      
        <category>javascript</category>
      
        <category>technology</category>
      
      
      <content:encoded>&lt;p&gt;A while ago I wrote an &lt;a href=&#34;https://devcenter.heroku.com/articles/s3-upload-python&#34; target=&#34;_blank&#34; rel=&#34;noopener noreferrer&#34; &gt;article&lt;/a&gt; for &lt;a href=&#34;https://heroku.com&#34; target=&#34;_blank&#34; rel=&#34;noopener noreferrer&#34; &gt;Heroku&lt;/a&gt;’s Dev Center on carrying out direct uploads to S3 using a Python app for signing the PUT request. Specifically, the article focussed on Flask but the concept is also applicable to most other Python web frameworks.&lt;/p&gt;
&lt;p&gt;I’ve recently had to implement something similar, but this time as part of an &lt;a href=&#34;http://nodejs.org&#34; target=&#34;_blank&#34; rel=&#34;noopener noreferrer&#34; &gt;Node.js&lt;/a&gt; application. Since the only difference between the two approaches is literally just the endpoint used to return a signed request URL, I thought I’d post an update on how the endpoint could be constructed in Node.&lt;/p&gt;
&lt;p&gt;The front-end code in the companion repository demonstrates an example of how the endpoint can be queried to retrieve the signed URL, and is available &lt;a href=&#34;https://github.com/willwebberley/FlaskDirectUploader/blob/master/templates/account.html&#34; target=&#34;_blank&#34; rel=&#34;noopener noreferrer&#34; &gt;here&lt;/a&gt;. Take a look at that repository’s README for information on the front-end dependencies.&lt;/p&gt;
&lt;p&gt;The full example referenced by the Python article is in a &lt;a href=&#34;https://github.com/willwebberley/FlaskDirectUploader&#34; target=&#34;_blank&#34; rel=&#34;noopener noreferrer&#34; &gt;repository&lt;/a&gt; hosted by GitHub and may be useful in providing more context.&lt;/p&gt;
</content:encoded>
    </item>
    
    <item>
      <title>Workshop Presentation in Germany</title>
      <link>https://wilw.dev/blog/2013/10/05/workshop-presentation-in-germany/</link>
      <pubDate>Sat, 05 Oct 2013 17:00:00 +0000</pubDate>
      
      <guid>https://wilw.dev/blog/2013/10/05/workshop-presentation-in-germany/</guid>
      
        <category>talk</category>
      
        <category>research</category>
      
      
      <content:encoded>&lt;p&gt;Last week I visited Karlsruhe, in Germany, to give a presentation accompanying a recently-accepted paper. The paper, “Inferring the Interesting Tweets in Your Network”, was in the proceedings of the Workshop on Analyzing Social Media for the Benefit of Society (&lt;a href=&#34;http://www.cs.cf.ac.uk/cosmos/node/12&#34; target=&#34;_blank&#34; rel=&#34;noopener noreferrer&#34; &gt;Society 2.0&lt;/a&gt;), which was part of the Third International Conference on Social Computing and its Applications (&lt;a href=&#34;http://socialcloud.aifb.uni-karlsruhe.de/confs/SCA2013/&#34; target=&#34;_blank&#34; rel=&#34;noopener noreferrer&#34; &gt;SCA&lt;/a&gt;).&lt;/p&gt;
&lt;p&gt;Although I only attended the first workshop day, there was a variety of interesting talks on social media and crowdsourcing. My own talk went well and there was some useful feedback from the attendees.&lt;/p&gt;
&lt;p&gt;I presented my recent work on the use of machine learning techniques to help in identifying interesting information in Twitter. I rounded up some of the results from the Twinterest experiment we ran a few months ago and discussed how this helped address the notion of information &lt;em&gt;relevance&lt;/em&gt; as an extension to global &lt;em&gt;interestingness&lt;/em&gt;.&lt;/p&gt;
&lt;p&gt;I hadn’t been to Germany before this, so it was also a culturally-interesting visit. I was only there for two nights but I tried to make the most of seeing some of Karlsruhe and enjoying the traditional food and local beers!&lt;/p&gt;
</content:encoded>
    </item>
    
    <item>
      <title>CasaStream</title>
      <link>https://wilw.dev/blog/2013/09/14/casastream/</link>
      <pubDate>Sat, 14 Sep 2013 17:00:00 +0000</pubDate>
      
      <guid>https://wilw.dev/blog/2013/09/14/casastream/</guid>
      
        <category>project</category>
      
        <category>linux</category>
      
        <category>technology</category>
      
      
      <content:encoded>&lt;p&gt;In my &lt;a href=&#34;https://wilw.dev/blog/2013/09/02/zoned-network-sound-streaming-the-problem&#34;&gt;last post&lt;/a&gt; I discussed methods for streaming music to different zones in the house. More specifically I wanted to be able to play music from one location and then listen to it in other rooms at the same time and in sync.&lt;/p&gt;
&lt;p&gt;After researching various methods, I decided to go with using a compressed MP3 stream over RTP. Other techniques introduced too much latency, did not provide the flexibility I required, or simply did not fulfill the requirements (e.g. not multiroom, only working with certain applications and non-simultaneous playback).&lt;/p&gt;
&lt;p&gt;To streamline the procedure of compressing the stream, broadcasting the stream, and receiving and playing the stream, I have started a project to create an easily-deployable wrapper around PulseAudio and VLC. The system, somewhat cheesily named &lt;a href=&#34;https://github.com/willwebberley/CasaStream&#34; target=&#34;_blank&#34; rel=&#34;noopener noreferrer&#34; &gt;CasaStream&lt;/a&gt; and currently written primarily in Python, relies on a network containing one machine running a CasaStream Master server and any number of machines running a CasaStream Slave server.&lt;/p&gt;
&lt;p&gt;&lt;img src=&#34;https://wilw.dev/media/blog/casastream1.png&#34; alt=&#34;Casastream interface&#34;&gt;&lt;/p&gt;
&lt;p&gt;The Master server is responsible for compressing and broadcasting the stream, and the Slaves receive and play the stream back through connected speakers. Although the compression is relatively resource-intensive (at least, for the moment), the Slave server is lightweight enough to be run on low-powered devices, such as the Raspberry Pi. Any machine that is powerful enough to run the Master could also simultaneously run a Slave, so a dedicated machine to serve the music alone is not required.&lt;/p&gt;
&lt;p&gt;&lt;img src=&#34;https://wilw.dev/media/blog/casastream2.png&#34; alt=&#34;Casastream interface&#34;&gt;&lt;/p&gt;
&lt;p&gt;The Master server also runs a web interface, allowing enabling of the system and to disable and enable Slaves. Slave servers are automatically discovered by the Master, though it is possible to alter the scan range from the web interface also. In addition, the selection of audio sources to stream (and their output volumes) and the renaming of Slaves are available as options. Sound sources are usually automatically detected by PulseAudio (if it is running), so there is generally no manual intervention required to ‘force’ the detection of sources.&lt;/p&gt;
&lt;p&gt;My current setup consists of a Master server running on a desktop machine in the kitchen, and Slave servers running on various other machines throughout the house (including the same kitchen desktop connected to some orbital speakers and a Raspberry Pi connected to the surround sound in the living room). When all running, there is no notable delay between the audio output in the different rooms.&lt;/p&gt;
&lt;p&gt;There are a few easily-installable dependencies required to run both servers. Both require Python (works on V2.*, but I haven’t tested on V3), and both require the Flask microframework and VLC. For a full list, please see the &lt;a href=&#34;https://github.com/willwebberley/CasaStream/blob/master/README.md&#34; target=&#34;_blank&#34; rel=&#34;noopener noreferrer&#34; &gt;README&lt;/a&gt; at the project’s home, which also provides more information on the installation and use.&lt;/p&gt;
&lt;p&gt;Unfortunately, there are a couple of caveats: firstly, the system is not reliable over WLAN (the sound gets pretty choppy), so a wired connection is recommended. Secondly, if using ethernet-over-power to mitigate the first caveat, then you may experience sound dropouts every 4-5 minutes. To help with this problem, the Slave servers are set to restart the stream every four minutes (by default).&lt;/p&gt;
&lt;p&gt;This is quite an annoying issue, however, since having short sound interruptions every few minutes is very noticeable. Some of my next steps with this project, therefore, are based around trying to find a better fix for this. In addition, I’d like to reduce the dependency footprint (the Slave servers really don’t need to use a fully-fledged web server), reduce the power requirements at both ends, and to further automate the installation process.&lt;/p&gt;
</content:encoded>
    </item>
    
    <item>
      <title>Zoned Network Sound-Streaming: The Problem</title>
      <link>https://wilw.dev/blog/2013/09/02/zoned-network-sound-streaming-the-problem/</link>
      <pubDate>Mon, 02 Sep 2013 17:00:00 +0000</pubDate>
      
      <guid>https://wilw.dev/blog/2013/09/02/zoned-network-sound-streaming-the-problem/</guid>
      
        <category>linux</category>
      
        <category>technology</category>
      
      
      <content:encoded>&lt;p&gt;For a while, now, I have been looking for a reliable way to manage zoned music-playing around the house. The general idea is that I’d like to be able to play music from a central point and have it streamed over the network to a selection of receivers, which could be remotely turned on and off when required, but still allow for multiple receivers to play simulataneously.&lt;/p&gt;
&lt;p&gt;Apple’s &lt;a href=&#34;http://www.apple.com/uk/airplay/&#34; target=&#34;_blank&#34; rel=&#34;noopener noreferrer&#34; &gt;AirPlay&lt;/a&gt; has supported this for a while now, but requires the purchasing of AirPlay compatible hardware, which is expensive. It’s also very iTunes-based - which is something that I do not use.&lt;/p&gt;
&lt;p&gt;Various open-source tools also allow network streaming. &lt;a href=&#34;http://www.icecast.org/&#34; target=&#34;_blank&#34; rel=&#34;noopener noreferrer&#34; &gt;Icecast&lt;/a&gt; (through the use of &lt;a href=&#34;https://code.google.com/p/darkice/&#34; target=&#34;_blank&#34; rel=&#34;noopener noreferrer&#34; &gt;Darkice&lt;/a&gt;) allows clients to stream from a multimedia server, but this causes pretty severe latency in playback between clients (ranging up to around 20 seconds, I’ve found) - not a good solution in a house!&lt;/p&gt;
&lt;p&gt;&lt;a href=&#34;http://www.freedesktop.org/wiki/Software/PulseAudio/&#34; target=&#34;_blank&#34; rel=&#34;noopener noreferrer&#34; &gt;PulseAudio&lt;/a&gt; is partly designed around being able to work over the network, and supports the discovery of other PulseAudio sinks on the LAN and the selection a sound card to transmit to through TCP. This doesn’t seem to support multiple sound card sinks very well, however.&lt;/p&gt;
&lt;p&gt;PulseAudio’s other network feature is its RTP broadcasting, and this seemed the most promising avenue for progression in solving this problem. RTP utilises UDP, and PulseAudio effecively uses this to broadcast its sound to any devices on the network that might be listening on the broadcast address. This means that one server could be run and sink devices could be set up simply to receive the RTP stream on demand - perfect!&lt;/p&gt;
&lt;p&gt;However, in practice, this turned out not to work very well. With RTP enabled, PulseAudio would entirely flood the network with sound packets. Although this isn’t a problem for devices with a wired connection, any devices connected wirelessly to the network would be immediately disassociated from the access point due to the complete saturation of PulseAudio’s packets being sent over the airwaves.&lt;/p&gt;
&lt;p&gt;This couldn’t be an option in a house where smartphones, games consoles, laptops, and so on require the WLAN. After researching this problem a fair bit (and finding many others experiencing the same issues), I found &lt;a href=&#34;http://www.freedesktop.org/wiki/Software/PulseAudio/Documentation/User/Network/RTP/&#34; target=&#34;_blank&#34; rel=&#34;noopener noreferrer&#34; &gt;this page&lt;/a&gt;, which describes various methods for using RTP streaming from PulseAudio and includes (at the bottom) the key that could fix my problems - the notion of compressing the audio into MP3 format (or similar) before broadcasting it.&lt;/p&gt;
&lt;p&gt;Trying this technique worked perfectly, and did not cause network floods anywhere nearly as severely as the uncompressed sound stream; wireless clients no longer lost access to the network once the stream was started and didn’t seem to lose any noticeable QoS at all. In addition, when multiple clients connected, the sound output would be nearly entirely simultaneous (at least after a few seconds to warm up).&lt;/p&gt;
&lt;p&gt;Unfortunately, broadcasting still didn’t work well over WLAN (sound splutters and periodic drop-outs), so the master server and any sound sinks would need to be on a wired network. This is a small price to pay, however, and I am happy to live with a few Ethernet-over-power devices around the house. The next stage is to think about what to use as sinks. Raspberry Pis should be powerful enough and are &lt;em&gt;significantly&lt;/em&gt; cheaper than Apple’s equivalent. They would also allow me to use existing sound systems in some rooms (e.g. the surround-sound in the living room), and other simple speaker setups in others. I also intend to write a program around PulseAudio to streamline the streaming process and a server for discovering networked sinks.&lt;/p&gt;
&lt;p&gt;I will write an update when I have made any more progress on this!&lt;/p&gt;
</content:encoded>
    </item>
    
    <item>
      <title>A rather French week</title>
      <link>https://wilw.dev/blog/2013/08/31/a-rather-french-week/</link>
      <pubDate>Sat, 31 Aug 2013 17:00:00 +0000</pubDate>
      
      <guid>https://wilw.dev/blog/2013/08/31/a-rather-french-week/</guid>
      
        <category>life</category>
      
        <category>holiday</category>
      
      
      <content:encoded>&lt;p&gt;I recently spent a week in France as part of a holiday with some of my family. Renting houses for a couple of weeks in France or Italy each summer has almost become a bit of a tradition, and it’s good to have a relax and a catch-up for a few days. They have been the first proper few days (other than the &lt;!-- raw HTML omitted --&gt;decking-building adventure&lt;!-- raw HTML omitted --&gt; back in March) I have had away from University in 2013, so I felt it was well-deserved!&lt;/p&gt;
&lt;p&gt;&lt;img src=&#34;https://wilw.dev/media/blog/french-house.JPG&#34; alt=&#34;The house&#34;&gt;&lt;/p&gt;
&lt;p&gt;This year we stayed in the Basque Country of southern France, relatively near Biarritz, in a country farmhouse. Although we weren’t really within walking distance to anywhere, the house did come with a pool in the garden, with a swimmable river just beyond, and an amazing, peaceful setting.&lt;/p&gt;
&lt;p&gt;Strangely enough, there was no Internet installation at the house, and no cellular reception anywhere nearby. This took a bit of getting-used to, but after a while it became quite relaxing not having to worry about checking emails, texts, and Twitter.  The only thing to cause any stress was a crazed donkey, living in the field next door, who would start braying loudly at random intervals through the nights, waking everyone up.&lt;/p&gt;
&lt;p&gt;&lt;img src=&#34;https://wilw.dev/media/blog/french-gorge.JPG&#34; alt=&#34;French Gorge&#34;&gt;&lt;/p&gt;
&lt;p&gt;As might be expected, the food and drink was exceptional. Although we did end up eating in the house each evening (to save having someone sacrifice themselves to be the designated driver), the foods we bought from the markets were very good, and the fact that wine cost €1.50 per bottle from the local Intermarché gave very little to complain about.&lt;/p&gt;
&lt;p&gt;The majority of most days was spent away from the house, visiting local towns, the beaches and the Pyrenees. We spent a few afternoons walking in the mountains, with some spectacular scenery.&lt;/p&gt;
&lt;p&gt;&lt;img src=&#34;https://wilw.dev/media/blog/french-pyrenes.JPG&#34; alt=&#34;Pyrenees&#34;&gt;&lt;/p&gt;
</content:encoded>
    </item>
    
    <item>
      <title>Gower Tides v1.4</title>
      <link>https://wilw.dev/blog/2013/07/31/gower-tides-v1.4/</link>
      <pubDate>Wed, 31 Jul 2013 17:00:00 +0000</pubDate>
      
      <guid>https://wilw.dev/blog/2013/07/31/gower-tides-v1.4/</guid>
      
        <category>android</category>
      
        <category>technology</category>
      
      
      <content:encoded>&lt;p&gt;&lt;img src=&#34;https://will.now.sh/static/media/v1-4_surf.png&#34; alt=&#34;Surf forecasts&#34;&gt;&lt;/p&gt;
&lt;p&gt;Last week I released a new version of the tides Android app I’m currently developing.&lt;/p&gt;
&lt;p&gt;The idea of the application was initially to simply display the tidal times and patterns for the Gower Peninsula, and that this should be possible without  a data connection. Though, as the time has gone by, I keep finding more and more things that can be added!&lt;/p&gt;
&lt;p&gt;The latest update saw the introduction of 5-day surf forecasts for four Gower locations - Llangennith, Langland, Caswell Bay, and Hunts Bay. All the surf data comes from &lt;a href=&#34;http://magicseaweed.com&#34; target=&#34;_blank&#34; rel=&#34;noopener noreferrer&#34; &gt;Magic Seaweed&lt;/a&gt;’s API (which I &lt;a href=&#34;https://wilw.dev/blog/2013/07/03/magic-seaweeds-awesome-new-api/&#34;&gt;talked about&lt;/a&gt; last time).&lt;/p&gt;
&lt;p&gt;&lt;img src=&#34;https://flyingsparx.net/static/media/v1-4_location.png&#34; alt=&#34;Location choices&#34;&gt;&lt;/p&gt;
&lt;p&gt;he surf forecasts are shown, for each day they are available, as a horizontal scroll-view, allowing users to scroll left and right within that day to view the forecast at different times of the day (in 3-hourly intervals).&lt;/p&gt;
&lt;p&gt;Location selection is handled by a dialog popup, which shows a labelled map and a list of the four available locations in a list view.&lt;/p&gt;
&lt;p&gt;The &lt;a href=&#34;https://github.com/willwebberley/GowerTidesBackend&#34; target=&#34;_blank&#34; rel=&#34;noopener noreferrer&#34; &gt;backend support&lt;/a&gt; for the application was modified to now also support 30-minute caching of surf data on a per-location basis (i.e. new calls to Magic Seaweed would not be made if the requested &lt;em&gt;location&lt;/em&gt; had been previously pulled in the last 30 minutes). The complete surf and weather data is then shipped back to the phone as one JSON structure.&lt;/p&gt;
&lt;p&gt;&lt;img src=&#34;https://flyingsparx.net/static/media/v1-4_tides.png&#34; alt=&#34;Tides view update&#34;&gt;&lt;/p&gt;
&lt;p&gt;Other updates were smaller but included an overhaul of the UI (the tide table now looks a bit nicer), additional licensing information, more speedy database interaction, and so on.&lt;/p&gt;
&lt;p&gt;If you are interested in the source, then that is available &lt;a href=&#34;https://github.com/willwebberley/GowerTides&#34; target=&#34;_blank&#34; rel=&#34;noopener noreferrer&#34; &gt;here&lt;/a&gt;, and the app itself is on &lt;a href=&#34;https://play.google.com/store/apps/details?id=net.willwebberley.gowertides&amp;hl=en&#34; target=&#34;_blank&#34; rel=&#34;noopener noreferrer&#34; &gt;Google Play&lt;/a&gt;. If you have any ideas, feedback or general comments, then please let me know!&lt;/p&gt;
</content:encoded>
    </item>
    
    <item>
      <title>Magic Seaweed&#39;s Awesome New API</title>
      <link>https://wilw.dev/blog/2013/07/03/magic-seaweeds-awesome-new-api/</link>
      <pubDate>Wed, 03 Jul 2013 17:00:00 +0000</pubDate>
      
      <guid>https://wilw.dev/blog/2013/07/03/magic-seaweeds-awesome-new-api/</guid>
      
        <category>project</category>
      
        <category>android</category>
      
        <category>technology</category>
      
      
      <content:encoded>&lt;p&gt;Back in March, I emailed &lt;a href=&#34;http://magicseaweed.com&#34; target=&#34;_blank&#34; rel=&#34;noopener noreferrer&#34; &gt;Magic Seaweed&lt;/a&gt; to ask them if they had a public API for their surf forecast data. They responded that they didn’t at the time, but that it was certainly on their to-do list. I am interested in the marine data for my &lt;a href=&#34;https://play.google.com/store/apps/details?id=net.willwebberley.gowertides&#34; target=&#34;_blank&#34; rel=&#34;noopener noreferrer&#34; &gt;Gower Tides&lt;/a&gt; application.&lt;/p&gt;
&lt;p&gt;Yesterday, I visited their website to have a look at the surf reports and some photos, when I noticed the presence of a &lt;a href=&#34;http://magicseaweed.com/developer/api&#34; target=&#34;_blank&#34; rel=&#34;noopener noreferrer&#34; &gt;Developer&lt;/a&gt; link in the footer of the site. It linked to pages about their new API, with an overview describing exactly what I wanted.&lt;/p&gt;
&lt;p&gt;Since the API is currently in beta, I emailed them requesting a key, which they were quick to respond with and helpfully included some further example request usages. They currently do not have any strict rate limits in place, but instead have a few &lt;a href=&#34;http://magicseaweed.com/developer/terms-and-conditions&#34; target=&#34;_blank&#34; rel=&#34;noopener noreferrer&#34; &gt;fair practice terms&lt;/a&gt; to discourage developers from going a bit trigger happy on API requests. They also request that you use a hyperlinked logo to accredit the data back to them. Due to caching, I will not have to make too many requests (since the application will preserve ‘stale’ data for 30 minutes before refreshing from Magic Seaweed, when requested), so hopefully that will keep the app’s footprint down.&lt;/p&gt;
&lt;p&gt;I have written the app’s new &lt;a href=&#34;https://github.com/willwebberley/GowerTidesBackend&#34; target=&#34;_blank&#34; rel=&#34;noopener noreferrer&#34; &gt;backend support&lt;/a&gt; for handling and caching the surf data ready for incorporating into the Android app soon. So far, the experience has been really good, with the API responding with lots of detailed information - almost matching the data behind their own &lt;a href=&#34;http://magicseaweed.com/Llangennith-Rhossili-Surf-Report/32/&#34; target=&#34;_blank&#34; rel=&#34;noopener noreferrer&#34; &gt;surf forecasts&lt;/a&gt;. Hopefully they won’t remove any of the features when they properly release it!&lt;/p&gt;
</content:encoded>
    </item>
    
    <item>
      <title>Accidental Kernel Upgrades on Digital Ocean</title>
      <link>https://wilw.dev/blog/2013/06/20/accidental-kernel-upgrades-on-digital-ocean/</link>
      <pubDate>Thu, 20 Jun 2013 17:00:00 +0000</pubDate>
      
      <guid>https://wilw.dev/blog/2013/06/20/accidental-kernel-upgrades-on-digital-ocean/</guid>
      
        <category>linux</category>
      
        <category>digitalocean</category>
      
        <category>technology</category>
      
      
      <content:encoded>&lt;p&gt;I today issued a full upgrade of the server at flyingsparx.net, which is hosted by &lt;a href=&#34;https://www.digitalocean.com&#34; target=&#34;_blank&#34; rel=&#34;noopener noreferrer&#34; &gt;Digital Ocean&lt;/a&gt;. By default, on Arch, this will upgrade every currently-installed package (where there is a counterpart in the official repositories), including the Linux kernel and the kernel headers.&lt;/p&gt;
&lt;p&gt;Digital Ocean maintain their own kernel versions and do not currently allow kernel switching, which is something I completely forgot. I rebooted the machine and tried re-connecting, but SSH couldn’t find the host. Digital Ocean’s website provides a console for connecting to the instance (or ‘droplet’) through VNC, which I used, through which I discovered that none of the network interfaces (except the loopback) were being brought up. I tried everything I could think of to fix this, but without being able to connect the droplet to the Internet, I was unable to download any other packages.&lt;/p&gt;
&lt;p&gt;Eventually, I contacted DO’s support, who were super quick in replying. They pointed out that the upgrade may have also updated the kernel (which, of course, it had), and that therefore the modules for networking weren’t going to load properly. I restored the droplet from one of the automatic backups, swapped the kernel back using DO’s web console, rebooted and things were back to where they should be.&lt;/p&gt;
&lt;p&gt;The fact that these things can be instantly fixed from their console and their quick customer support make Digital Ocean awesome! If they weren’t possible then this would have been a massive issue, since the downtime also took out this website and the backend for a couple of mobile apps. If you use an Arch instance, then there is a &lt;a href=&#34;https://www.digitalocean.com/community/articles/pacman-syu-kernel-update-solved-how-to-ignore-arch-kernel-upgrades&#34; target=&#34;_blank&#34; rel=&#34;noopener noreferrer&#34; &gt;community article&lt;/a&gt; on their website explaining how to make pacman ignore kernel upgrades and to stop this from happening.&lt;/p&gt;
</content:encoded>
    </item>
    
    <item>
      <title>WekaPy</title>
      <link>https://wilw.dev/blog/2013/06/12/wekapy/</link>
      <pubDate>Wed, 12 Jun 2013 17:00:00 +0000</pubDate>
      
      <guid>https://wilw.dev/blog/2013/06/12/wekapy/</guid>
      
        <category>weka</category>
      
        <category>python</category>
      
        <category>machinelearning</category>
      
        <category>project</category>
      
        <category>technology</category>
      
      
      <content:encoded>&lt;p&gt;Over the last few months, I’ve started to use Weka more and more. &lt;a href=&#34;http://www.cs.waikato.ac.nz/ml/weka/&#34; target=&#34;_blank&#34; rel=&#34;noopener noreferrer&#34; &gt;Weka&lt;/a&gt; is a toolkit, written in Java, that I use to create models with which to make classifications on data sets.&lt;/p&gt;
&lt;p&gt;It features a wide variety of different machine learning algorithms (although I’ve used the logistic regressions and Bayesian networks most) which can be trained on data in order to make classifications (or ‘predictions’) for sets of instances.&lt;/p&gt;
&lt;p&gt;Weka comes as a GUI application and also as a library of classes for use from the command line or in Java applications. I needed to use it to create some large models and several smaller ones, and using the GUI version makes the process of training the model, testing it with data and parsing the classifications a bit clunky. I needed to automate the process a bit more.&lt;/p&gt;
&lt;p&gt;Nearly all of the development work for my PhD has been in Python, and it’d be nice to just plug in some machine learning processes over my existing code. Whilst there are some wrappers for Weka written for Python (&lt;a href=&#34;https://github.com/chrisspen/weka&#34; target=&#34;_blank&#34; rel=&#34;noopener noreferrer&#34; &gt;this project&lt;/a&gt;, &lt;a href=&#34;https://pypi.python.org/pypi/PyWeka&#34; target=&#34;_blank&#34; rel=&#34;noopener noreferrer&#34; &gt;PyWeka&lt;/a&gt;, etc.), most of them feel unfinished, are under-documented or are essentially just instructions on how to use &lt;a href=&#34;http://www.jython.org/&#34; target=&#34;_blank&#34; rel=&#34;noopener noreferrer&#34; &gt;Jython&lt;/a&gt;.&lt;/p&gt;
&lt;p&gt;So, I started work on &lt;a href=&#34;https://github.com/willwebberley/WekaPy&#34; target=&#34;_blank&#34; rel=&#34;noopener noreferrer&#34; &gt;WekaPy&lt;/a&gt;, a simple wrapper that allows efficient and Python-friendly integration with Weka. It basically just involves subprocesses to execute Weka from the command line, but also includes several areas of functionality aimed to provide more of a seamless and simple experience to the user.&lt;/p&gt;
&lt;p&gt;I haven’t got round to writing proper documentation yet, but most of the current functionality is explained and demo’d through examples &lt;a href=&#34;https://github.com/willwebberley/WekaPy#example-usage&#34; target=&#34;_blank&#34; rel=&#34;noopener noreferrer&#34; &gt;here&lt;/a&gt;. Below is an example demonstrating its ease of use.&lt;/p&gt;
&lt;pre tabindex=&#34;0&#34;&gt;&lt;code&gt;model = Model(classifier_type = &#34;bayes.BayesNet&#34;)
model.train(training_file = &#34;train.arff&#34;)
model.test(test_file = &#34;test.arff&#34;)
&lt;/code&gt;&lt;/pre&gt;&lt;p&gt;All that is needed is to instantiate the model with your desired classifier, train it with some training data and then test it against your test data. The predictions can then be easily extracted from the model as shown &lt;a href=&#34;https://github.com/willwebberley/WekaPy#accessing-the-predictions&#34; target=&#34;_blank&#34; rel=&#34;noopener noreferrer&#34; &gt;in the documentation&lt;/a&gt;.&lt;/p&gt;
&lt;p&gt;I hope to continue updating the library and improving the documentation when I get a chance! Please let me know if you have any ideas for functionality.&lt;/p&gt;
</content:encoded>
    </item>
    
    <item>
      <title>Gower Tides Open-Sourced</title>
      <link>https://wilw.dev/blog/2013/05/26/gower-tides-open-sourced/</link>
      <pubDate>Sun, 26 May 2013 17:00:00 +0000</pubDate>
      
      <guid>https://wilw.dev/blog/2013/05/26/gower-tides-open-sourced/</guid>
      
        <category>android</category>
      
        <category>project</category>
      
        <category>technology</category>
      
      
      <content:encoded>&lt;p&gt;This is just a quick post to mention that I have made the source for the &lt;a href=&#34;https://play.google.com/store/apps/details?id=net.willwebberley.gowertides&#34; target=&#34;_blank&#34; rel=&#34;noopener noreferrer&#34; &gt;Gower Tides&lt;/a&gt; app on Google Play public.&lt;/p&gt;
&lt;p&gt;The source repository is available on &lt;a href=&#34;https://github.com/willwebberley/GowerTides&#34; target=&#34;_blank&#34; rel=&#34;noopener noreferrer&#34; &gt;GitHub&lt;/a&gt;. From the repository I have excluded:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;Images &amp; icons&lt;/strong&gt; - It is not my place to distribute graphics not owned or created by me. Authors are credited in the repo’s README and in the application.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;External libraries&lt;/strong&gt; - The app requires a graphing package and a class to help with handling locally-packaged SQLite databases. Links to both are also included in the repo’s README.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Tidal data&lt;/strong&gt; - The tidal data displayed in the app has also been excluded. However, the format for the data stored by the app should be relatively obvious from its access in the &lt;a href=&#34;https://github.com/willwebberley/GowerTides/blob/master/src/net/willwebberley/gowertides/utils/DayDatabase.java&#34; target=&#34;_blank&#34; rel=&#34;noopener noreferrer&#34; &gt;source&lt;/a&gt;.&lt;/li&gt;
&lt;/ul&gt;
</content:encoded>
    </item>
    
    <item>
      <title>Contribution to Heroku Dev Center</title>
      <link>https://wilw.dev/blog/2013/05/07/contribution-to-heroku-dev-center/</link>
      <pubDate>Tue, 07 May 2013 17:00:00 +0000</pubDate>
      
      <guid>https://wilw.dev/blog/2013/05/07/contribution-to-heroku-dev-center/</guid>
      
        <category>contribution</category>
      
        <category>heroku</category>
      
        <category>python</category>
      
        <category>aws</category>
      
        <category>s3</category>
      
      
      <content:encoded>&lt;p&gt;The &lt;a href=&#34;https://devcenter.heroku.com&#34; target=&#34;_blank&#34; rel=&#34;noopener noreferrer&#34; &gt;Heroku Dev Center&lt;/a&gt; is a repository of guides and articles to provide support for those writing applications to be run on the &lt;a href=&#34;https://heroku.com&#34; target=&#34;_blank&#34; rel=&#34;noopener noreferrer&#34; &gt;Heroku&lt;/a&gt; platform.&lt;/p&gt;
&lt;p&gt;I recently contributed an article for carrying out &lt;a href=&#34;https://devcenter.heroku.com/articles/s3-upload-python&#34; target=&#34;_blank&#34; rel=&#34;noopener noreferrer&#34; &gt;Direct to S3 File Uploads in Python&lt;/a&gt;, as I have previously used a very similar approach to interface with Amazon’s Simple Storage Service in one of my apps running on Heroku.&lt;/p&gt;
&lt;p&gt;The approach discussed in the article focuses on avoiding as much server-side processing as possible, with the aim of preventing the app’s web dynos from becoming too tied up and unable to respond to further requests. This is done by using client-side JavaScript to asynchronously carry out the upload directly to S3 from the web browser. The only necessary server-side processing involves the generation of a temporarily-signed (using existing AWS credentials) request, which is returned to the browser in order to allow the JavaScript to successfully make the final &lt;code&gt;PUT&lt;/code&gt; request.&lt;/p&gt;
&lt;p&gt;The guide’s &lt;a href=&#34;https://github.com/willwebberley/FlaskDirectUploader&#34; target=&#34;_blank&#34; rel=&#34;noopener noreferrer&#34; &gt;companion git repository&lt;/a&gt; hopes to demonstrate a simple use-case for this system. As with all of the Heroku Dev Center articles, if you have any feedback (e.g. what could be improved, what helped you, etc.), then please do provide it!&lt;/p&gt;
</content:encoded>
    </item>
    
    <item>
      <title>eartub.es</title>
      <link>https://wilw.dev/blog/2013/04/25/eartub.es/</link>
      <pubDate>Thu, 25 Apr 2013 17:00:00 +0000</pubDate>
      
      <guid>https://wilw.dev/blog/2013/04/25/eartub.es/</guid>
      
        <category>event</category>
      
        <category>project</category>
      
        <category>cardiffuniversity</category>
      
      
      <content:encoded>&lt;p&gt;Last weekend I went to &lt;a href=&#34;http://www.cs.cf.ac.uk/hackathon&#34; target=&#34;_blank&#34; rel=&#34;noopener noreferrer&#34; &gt;CFHack Open Sauce Hackathon&lt;/a&gt;. I worked in a team with &lt;a href=&#34;http://christopher-gwilliams.com&#34; target=&#34;_blank&#34; rel=&#34;noopener noreferrer&#34; &gt;Chris&lt;/a&gt;, &lt;a href=&#34;https://twitter.com/OnyxNoir&#34; target=&#34;_blank&#34; rel=&#34;noopener noreferrer&#34; &gt;Ross&lt;/a&gt; and &lt;a href=&#34;http://users.cs.cf.ac.uk/M.P.John/&#34; target=&#34;_blank&#34; rel=&#34;noopener noreferrer&#34; &gt;Matt&lt;/a&gt;.&lt;/p&gt;
&lt;p&gt;We started work on &lt;a href=&#34;http://eartub.es&#34; target=&#34;_blank&#34; rel=&#34;noopener noreferrer&#34; &gt;eartub.es&lt;/a&gt;, which is a web application for suggesting movies based on their sound tracks. We had several ideas for requirements we wanted to meet but, due to the nature of hackathons, we didn’t do nearly as much as what we thought we would!&lt;/p&gt;
&lt;p&gt;For now, eartubes allows you to search for a movie (from a 2.5 million movie database) and view other movies with similar soundtracks. This is currently based on cross matching the composer between movies, but more in-depth functionality is still in the works. We have nearly completed Last.fm integration, which would allow the app to suggest movies from your favourite and most listened-to music, and are working towards genre-matching and other, more complex, learning techniques. The registration functionality is disabled while we add this extra stuff.&lt;/p&gt;
&lt;p&gt;The backend is written in Python and runs as a Flask application. Contrary to my usual preference, I worked on the front end of the application, but also wrote our internal API for Last.fm integration. It was a really fun experience, in which everyone got on with their own individual parts, and it was good to see the project come together at the end of the weekend.&lt;/p&gt;
&lt;p&gt;The project’s source is on &lt;a href=&#34;https://github.com/encima/eartubes&#34; target=&#34;_blank&#34; rel=&#34;noopener noreferrer&#34; &gt;GitHub&lt;/a&gt;.&lt;/p&gt;
</content:encoded>
    </item>
    
    <item>
      <title>flyingsparx.net On Digital Ocean</title>
      <link>https://wilw.dev/blog/2013/04/23/flyingsparx.net-on-digital-ocean/</link>
      <pubDate>Tue, 23 Apr 2013 17:00:00 +0000</pubDate>
      
      <guid>https://wilw.dev/blog/2013/04/23/flyingsparx.net-on-digital-ocean/</guid>
      
        <category>digitalocean</category>
      
        <category>technology</category>
      
      
      <content:encoded>&lt;p&gt;My hosting for &lt;a href=&#34;http://www.willwebberley.net&#34; target=&#34;_blank&#34; rel=&#34;noopener noreferrer&#34; &gt;my website&lt;/a&gt; has nearly expired, so I have been looking for renewal options.&lt;/p&gt;
&lt;p&gt;These days I tend to need to use servers for more than simple web-hosting, and most do not provide the flexibility that a VPS would. Having (mostly) full control over a properly-maintained virtual cloud server is so much more convenient, and allows you to do tonnes of stuff beyond simple web hosting.&lt;/p&gt;
&lt;p&gt;I have some applications deployed on &lt;a href=&#34;https://www.heroku.com&#34; target=&#34;_blank&#34; rel=&#34;noopener noreferrer&#34; &gt;Heroku&lt;/a&gt;, which is definitely useful and easy for this purpose, but I decided to complement this for my needs by buying a ‘droplet’ from &lt;a href=&#34;https://www.digitalocean.com&#34; target=&#34;_blank&#34; rel=&#34;noopener noreferrer&#34; &gt;Digital Ocean&lt;/a&gt;.&lt;/p&gt;
&lt;p&gt;Droplets are DO’s term for a server instance, and are super quick to set up (55 seconds from first landing at their site to a booted virtual server, they claim) and very reasonably priced. I started an Arch instance, quickly set up nginx, Python and uwsgi, and started this blog and site as a Python app running on the Flask microframework.&lt;/p&gt;
&lt;p&gt;So far, I’ve had no issues, and everything seems to work quickly and smoothly. If all goes to plan, over the next few months I’ll migrate some more stuff over, including the backend for the Gower Tides app.&lt;/p&gt;
</content:encoded>
    </item>
    
    <item>
      <title>Trials of Eduroam</title>
      <link>https://wilw.dev/blog/2013/04/16/trials-of-eduroam/</link>
      <pubDate>Tue, 16 Apr 2013 17:00:00 +0000</pubDate>
      
      <guid>https://wilw.dev/blog/2013/04/16/trials-of-eduroam/</guid>
      
        <category>linux</category>
      
        <category>wifi</category>
      
        <category>technology</category>
      
      
      <content:encoded>&lt;p&gt;I’ve been having trouble connecting to Eduroam, at least reliably and persistently, without heavy desktop environments or complicated network managers. Eduroam is the wireless networking service used by many Universities in Europe, and whilst it would probably work fine using the tools provided by heavier DEs, I wanted something that could just run quickly and independently.&lt;/p&gt;
&lt;p&gt;Many approaches require the editing of loads of config files (especially true for &lt;code&gt;netcfg&lt;/code&gt;), which would need altering again after things like password changes. The approach I used (for Arch Linux) is actually really simple and involves the use of the user-contributed &lt;code&gt;wicd-eduroam&lt;/code&gt; package available in the &lt;a href=&#34;https://aur.archlinux.org/packages/wicd-eduroam/&#34; target=&#34;_blank&#34; rel=&#34;noopener noreferrer&#34; &gt;Arch User Repository&lt;/a&gt;.&lt;/p&gt;
&lt;p&gt;Obviously, &lt;code&gt;wicd-eduroam&lt;/code&gt; is related to, and depends on, &lt;code&gt;wicd&lt;/code&gt;, a handy network connection manager, so install that first:&lt;/p&gt;
&lt;pre tabindex=&#34;0&#34;&gt;&lt;code&gt;# pacman -S wicd
$ yaourt -S wicd-eduroam
&lt;/code&gt;&lt;/pre&gt;&lt;p&gt;(If you don’t use &lt;code&gt;yaourt&lt;/code&gt; download the &lt;a href=&#34;https://aur.archlinux.org/packages/wi/wicd-eduroam/wicd-eduroam.tar.gz&#34; target=&#34;_blank&#34; rel=&#34;noopener noreferrer&#34; &gt;tarball&lt;/a&gt; and build it using the &lt;code&gt;makepkg&lt;/code&gt; method.)&lt;/p&gt;
&lt;p&gt;&lt;code&gt;wicd&lt;/code&gt; can conflict with other network managers, so stop and disable them before starting and enabling &lt;code&gt;wicd&lt;/code&gt;. This will allow it to startup at boot time. e.g.:&lt;/p&gt;
&lt;pre tabindex=&#34;0&#34;&gt;&lt;code&gt;# systemctl stop NetworkManager
# systemctl disable NetworkManager
# systemctl start wicd
# systemctl enable wicd
&lt;/code&gt;&lt;/pre&gt;&lt;p&gt;Now start &lt;code&gt;wicd-client&lt;/code&gt; (or set it to autostart), let it scan for networks, and edit the properties of the network &lt;code&gt;eduroam&lt;/code&gt; Set the encryption type as &lt;code&gt;eduroam&lt;/code&gt; in the list, enter the username and password, click OK and then allow it to connect.&lt;/p&gt;
</content:encoded>
    </item>
    
    <item>
      <title>Cardiff Open Sauce Hackathon</title>
      <link>https://wilw.dev/blog/2013/04/11/cardiff-open-sauce-hackathon/</link>
      <pubDate>Thu, 11 Apr 2013 17:00:00 +0000</pubDate>
      
      <guid>https://wilw.dev/blog/2013/04/11/cardiff-open-sauce-hackathon/</guid>
      
        <category>event</category>
      
        <category>cardiffuniversity</category>
      
      
      <content:encoded>&lt;p&gt;Next week I, along with others in a team, am taking part in &lt;a href=&#34;http://www.cs.cf.ac.uk/hackathon/&#34; target=&#34;_blank&#34; rel=&#34;noopener noreferrer&#34; &gt;Cardiff Open Sauce Hackathon&lt;/a&gt;.&lt;/p&gt;
&lt;p&gt;If you’re in the area and feel like joining in for the weekend then sign up at the link above.&lt;/p&gt;
&lt;p&gt;he hackathon is a two-day event in which teams work to ‘hack together’ smallish projects, which will be open-sourced at the end of the weekend. Whilst we have a few ideas already for potential projects, if anyone has any cool ideas for something relatively quick, but useful, to make, then please let me know!&lt;/p&gt;
</content:encoded>
    </item>
    
    <item>
      <title>AJAX &#43; Python &#43; Amazon S3</title>
      <link>https://wilw.dev/blog/2013/04/05/ajax--python--amazon-s3/</link>
      <pubDate>Fri, 05 Apr 2013 17:00:00 +0000</pubDate>
      
      <guid>https://wilw.dev/blog/2013/04/05/ajax--python--amazon-s3/</guid>
      
        <category>python</category>
      
        <category>aws</category>
      
        <category>s3</category>
      
        <category>technology</category>
      
      
      <content:encoded>&lt;p&gt;I wanted a way in which users can seamlessly upload images for use in the Heroku application discussed in previous posts.&lt;/p&gt;
&lt;p&gt;Ideally, the image would be uploaded through AJAX as part of a data-entry form, but without having to refresh the page or anything else that would disrupt the user’s experience. As far as I know, barebones JQuery does not support AJAX uploads, but &lt;a href=&#34;http://www.malsup.com/jquery/form/#file-upload&#34; target=&#34;_blank&#34; rel=&#34;noopener noreferrer&#34; &gt;this handy plugin&lt;/a&gt; does.&lt;/p&gt;
&lt;h3 id=&#34;handling-the-upload-ajax&#34;&gt;Handling the upload (AJAX)&lt;/h3&gt;
&lt;p&gt;styled the file input nicely (in a similar way to &lt;a href=&#34;http://ericbidelman.tumblr.com/post/14636214755/making-file-inputs-a-pleasure-to-look-at&#34; target=&#34;_blank&#34; rel=&#34;noopener noreferrer&#34; &gt;this guy&lt;/a&gt;) and added the JS so that the upload is sent properly (and to the appropriate URL) when a change is detected to the input (i.e. the user does not need to click the ‘upload’ button to start the upload).&lt;/p&gt;
&lt;h3 id=&#34;receiving-the-upload-python&#34;&gt;Receiving the upload (Python)&lt;/h3&gt;
&lt;p&gt;he backend, as previously mentioned, is written in Python as part of a Flask app. Since Heroku’s customer webspace is read-only, uploads would have to be stored elsewhere. &lt;a href=&#34;http://boto.s3.amazonaws.com/index.html&#34; target=&#34;_blank&#34; rel=&#34;noopener noreferrer&#34; &gt;Boto&lt;/a&gt;’s a cool library for interfacing with various AWS products (including S3) and can easily be installed with &lt;code&gt;pip install boto&lt;/code&gt;. From this library, we’re going to need the &lt;code&gt;S3Connection&lt;/code&gt; and &lt;code&gt;Key&lt;/code&gt; classes:&lt;/p&gt;
&lt;pre tabindex=&#34;0&#34;&gt;&lt;code&gt;from boto.s3.connection import S3Connection
from boto.s3.key import Key
&lt;/code&gt;&lt;/pre&gt;&lt;p&gt;Now we can easily handle the transfer using the &lt;code&gt;request&lt;/code&gt; object exposed to Flask’s routing methods:&lt;/p&gt;
&lt;pre tabindex=&#34;0&#34;&gt;&lt;code&gt;file = request.files[&#39;file_input_name&#39;]
con = S3Connection(&lt;&#39;AWS_KEY&#39;&gt;, &lt;&#39;AWS_SECRET&#39;&gt;)
key = Key(con.get_bucket(&lt;&#39;BUCKET_NAME&#39;&gt;))
key.set_contents_from_file(file)
&lt;/code&gt;&lt;/pre&gt;&lt;p&gt;Go to the next step for the AWS details and the bucket name. Depending on where you chose your AWS location as (e.g. US, Europe, etc.), then your file will be accessible as something like &lt;code&gt;https://s3-eu-west-1.amazonaws.com/&lt;BUCKET_NAME&gt;/&lt;FILENAME&gt;&lt;/code&gt;. If you want, you can also set, among other things, stuff like the file’s mime type and access type:&lt;/p&gt;
&lt;pre tabindex=&#34;0&#34;&gt;&lt;code&gt;key.set_metadata(&#39;Content-Type&#39;, &#39;image/png&#39;)
key.set_acl(&#39;public-read&#39;)
&lt;/code&gt;&lt;/pre&gt;&lt;h3 id=&#34;setting-up-the-bucket-amazon-s3&#34;&gt;Setting up the bucket (Amazon S3)&lt;/h3&gt;
&lt;p&gt;Finally you’ll need to create the bucket. Create or log into your AWS account, go to the AWS console, choose your region (if you’re in Europe, then the Ireland one is probably the best choice) and enter the S3 section. Here, create a bucket (the name needs to be globally unique). Now, go to your account settings page to find your AWS access key and secret and plug these, along with the bucket name, into the appropriate places in your Python file.&lt;/p&gt;
&lt;p&gt;And that’s it. For large files, this may tie up your Heroku dynos a bit while they carry out the upload, so this technique is best for smaller files (especially if you’re only using the one web dyno). My example of a working implementation of this is available &lt;a href=&#34;https://github.com/willwebberley/niteowl-web/blob/master/api.py&#34; target=&#34;_blank&#34; rel=&#34;noopener noreferrer&#34; &gt;in this file&lt;/a&gt;.&lt;/p&gt;
</content:encoded>
    </item>
    
    <item>
      <title>Decking Building</title>
      <link>https://wilw.dev/blog/2013/03/30/decking-building/</link>
      <pubDate>Sat, 30 Mar 2013 17:00:00 +0000</pubDate>
      
      <guid>https://wilw.dev/blog/2013/03/30/decking-building/</guid>
      
        <category>life</category>
      
      
      <content:encoded>&lt;p&gt;&lt;img src=&#34;https://wilw.dev/media/blog/decking.png&#34; alt=&#34;A new decking&#34;&gt;&lt;/p&gt;
&lt;p&gt;I managed to turn about two tonnes of material into something vaguely resembling ‘decking’ in my back garden this weekend. It makes the area look much nicer, but whether it actually stays up is a completely different matter.&lt;/p&gt;
</content:encoded>
    </item>
    
    <item>
      <title>Gower Tides App Released</title>
      <link>https://wilw.dev/blog/2013/03/07/gower-tides-app-released/</link>
      <pubDate>Thu, 07 Mar 2013 17:00:00 +0000</pubDate>
      
      <guid>https://wilw.dev/blog/2013/03/07/gower-tides-app-released/</guid>
      
        <category>android</category>
      
        <category>project</category>
      
        <category>technology</category>
      
      
      <content:encoded>&lt;p&gt;A &lt;a href=&#34;https://wilw.dev/blog/2012/11/13-delving-into-android&#34;&gt;few posts back&lt;/a&gt;, I talked
about the development of an Android app for tide predictions for South Wales. This app is now on &lt;a href=&#34;https://play.google.com/store/apps/details?id=net.willwebberley.gowertides&#34; target=&#34;_blank&#34; rel=&#34;noopener noreferrer&#34; &gt;Google Play&lt;/a&gt;.&lt;/p&gt;
&lt;p&gt;If you live in South Wales and are vaguely interested in tides/weather, then you should probably download it :)&lt;/p&gt;
&lt;p&gt;The main advantage is that the app does not need any data connection to display the tidal data, which is useful in areas
with low signal. In future, I hope to add further features, such as a more accurate tide graph (using a proper ‘wave’),
surf reports, and just general UI updates.&lt;/p&gt;
</content:encoded>
    </item>
    
    <item>
      <title>ScriptSlide</title>
      <link>https://wilw.dev/blog/2013/02/18/scriptslide/</link>
      <pubDate>Mon, 18 Feb 2013 17:00:00 +0000</pubDate>
      
      <guid>https://wilw.dev/blog/2013/02/18/scriptslide/</guid>
      
        <category>javascript</category>
      
        <category>project</category>
      
        <category>technology</category>
      
      
      <content:encoded>&lt;p&gt;I’ve taken to writing most of my recent presentations in plain HTML (rather than using third-party software or services). I used
JavaScript to handle the appearance and ordering of slides.&lt;/p&gt;
&lt;p&gt;I bundled the JS into a single script, &lt;code&gt;js/scriptslide.js&lt;/code&gt; which can be configured
using the &lt;code&gt;js/config.js&lt;/code&gt; script.&lt;/p&gt;
&lt;p&gt;There is a &lt;a href=&#34;https://github.com/willwebberley/ScriptSlide&#34; target=&#34;_blank&#34; rel=&#34;noopener noreferrer&#34; &gt;GitHub repo&lt;/a&gt; for the code, along with example usage and instructions.&lt;/p&gt;
&lt;p&gt;Most configuration can be done by using the &lt;code&gt;js/config.js&lt;/code&gt; script, which supports many features including:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;Set the slide transition type (appear, fade, slide)&lt;/li&gt;
&lt;li&gt;Set the logos, page title, etc.&lt;/li&gt;
&lt;li&gt;Configure the colour scheme&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;Then simply create an HTML document, set some other styles (there is a template in &lt;code&gt;css/styles.css&lt;/code&gt;), and
put each slide inside &lt;code&gt;&lt;section&gt;...&lt;/section&gt;&lt;/code&gt; tags. The slide menu is then generated autmatically
when the page is loaded.&lt;/p&gt;
</content:encoded>
    </item>
    
    <item>
      <title>Research Poster Day</title>
      <link>https://wilw.dev/blog/2013/01/21/research-poster-day/</link>
      <pubDate>Mon, 21 Jan 2013 17:00:00 +0000</pubDate>
      
      <guid>https://wilw.dev/blog/2013/01/21/research-poster-day/</guid>
      
        <category>research</category>
      
        <category>cardiffuniversity</category>
      
      
      <content:encoded>&lt;p&gt;Each January the School of Computer Science hosts a poster day in order for the research students to demonstrate their current work to
other research students, research staff and undergraduates. The event lets members of the department see what other research is being done outside of their own group and gives researchers an opportunity to defend their research ideas.&lt;/p&gt;
&lt;p&gt;This year, I focused on my current research area, which is to do with inferring how interesting a Tweet is based on a comparison between simulated retweet patterns and the propagation behaviour demonstrated by the Tweet in Twitter itself. The poster highlights recent work in the build-up to this, a general overview of how the research works, and finishes with where I want to take this research in the future.&lt;/p&gt;
</content:encoded>
    </item>
    
    <item>
      <title>Delving into Android</title>
      <link>https://wilw.dev/blog/2012/11/13/delving-into-android/</link>
      <pubDate>Tue, 13 Nov 2012 17:00:00 +0000</pubDate>
      
      <guid>https://wilw.dev/blog/2012/11/13/delving-into-android/</guid>
      
        <category>android</category>
      
        <category>project</category>
      
        <category>technology</category>
      
      
      <content:encoded>&lt;p&gt;&lt;img src=&#34;https://wilw.dev/media/blog/tides-main.png&#34; alt=&#34;Tides Main Activity&#34;&gt;&lt;/p&gt;
&lt;p&gt;I’ve always been interested in the development of smartphone apps, but have never really had the opportunity
to actually hava a go. Whilst I’m generally OK with development on platforms I feel comfortable with, I’ve always
considered there to be no point in developing applications for wider use unless you have a good idea about first thinking
about the direction for it to go.&lt;/p&gt;
&lt;p&gt;My Dad is a keen surfer and has a watch which tells the tide changes as well as the time. It shows the next event (i.e. low- or high-tide)
and the time until that event, but he always complains about how inaccurate it is and how it never correctly predicts the tide
schedule for the places he likes to surf.&lt;/p&gt;
&lt;p&gt;He uses an Android phone, and so I thought I’d try making an app for him that would be more accurate than his watch, and
maybe provide more interesting features. The only tricky criterion, really, was that he needed it to predict the tides offline, since
the data reception is very poor in his area.&lt;/p&gt;
&lt;p&gt;I got to work on setting up a database of tidal data, based around the location he surfs in, and creating a basic UI in which to display it.
When packaging the application with an existing SQLite database, this &lt;a href=&#34;https://github.com/jgilfelt/android-sqlite-asset-helper&#34; target=&#34;_blank&#34; rel=&#34;noopener noreferrer&#34; &gt;helper class&lt;/a&gt; was particularly useful.&lt;/p&gt;
&lt;p&gt;&lt;img src=&#34;https://wilw.dev/media/blog/tides-settings.png&#34; alt=&#34;Tides Settings Activity&#34;&gt;&lt;/p&gt;
&lt;p&gt;A graphical UI seemed the best approach for displaying the data, so I
tried [AndroidPlot]](&lt;a href=&#34;http://androidplot.com/%29&#34; target=&#34;_blank&#34; rel=&#34;noopener noreferrer&#34; &gt;http://androidplot.com/)&lt;/a&gt;, a highly-customisable graphing
library, to show the tidal patterns day-by-day. This seemed to work OK (though not entirely accurately - tidal patterns form
more of a cosine wave rather than the zigzags my graph produced, but the general idea is there), so I added more features, such as
a tide table (the more traditional approach) and a sunrise and sunset timer.&lt;/p&gt;
&lt;p&gt;I showed him the app at this stage, and he decided it could be improved by adding weather forecasts. Obviously, preidcting the
weather cannot be done offline, so having sourced a decent &lt;a href=&#34;http://www.worldweatheronline.com/&#34; target=&#34;_blank&#34; rel=&#34;noopener noreferrer&#34; &gt;weather API&lt;/a&gt;,
I added the weather forecast for his area too. Due to the rate-limiting of World Weather Online, a cache is stored in a database
on the host for this website, which, when queried by the app, will make the request on the app’s behalf and store the data until
it is stale.&lt;/p&gt;
&lt;p&gt;I added a preferences activity for some general customisation, and that’s as far as I’ve currently got. In terms of development,
I guess it’s been a good introduction to the ideas behind various methodologies and features, such as the manifest file, networking,
local storage, preferences, and layout design. I’ll create a Github repository for it when I get round to it.&lt;/p&gt;
</content:encoded>
    </item>
    
    <item>
      <title>Seminar: Retweeting</title>
      <link>https://wilw.dev/blog/2012/10/10/seminar-retweeting/</link>
      <pubDate>Wed, 10 Oct 2012 17:00:00 +0000</pubDate>
      
      <guid>https://wilw.dev/blog/2012/10/10/seminar-retweeting/</guid>
      
        <category>talk</category>
      
        <category>research</category>
      
      
      <content:encoded>&lt;p&gt;I gave a seminar on my current research phase.&lt;/p&gt;
&lt;p&gt;I summarised my work over the past few months; in particular, the work on the network structure of Twitter, the way in which tweets
propagate through different network types, and the implications of this. I discussed the importance of precision and recall as metrics
for determining a timeline&#39;s quality and how this is altered through retweeting in different network types.&lt;/p&gt;
&lt;p&gt;I concluded by talking about my next area of research; how I may use the model used for the previous experimentation to determine if
a tweet is particularly interesting based on its features. Essentially, this boils down to showing that tweets are siginificantly
interesting (or uninteresting) by looking at how they compare to their &lt;em&gt;predicted&lt;/em&gt; retweet behaviours as produced by the model.&lt;/p&gt;
</content:encoded>
    </item>
    
    <item>
      <title>DigiSocial Hackathon</title>
      <link>https://wilw.dev/blog/2012/09/20/digisocial-hackathon/</link>
      <pubDate>Thu, 20 Sep 2012 09:15:00 +0000</pubDate>
      
      <guid>https://wilw.dev/blog/2012/09/20/digisocial-hackathon/</guid>
      
        <category>event</category>
      
        <category>cardiffuniversity</category>
      
      
      <content:encoded>&lt;p&gt;We recently held our DigiSocial Hackathon. This was a collaboration between the Schools of
Computer Science and Social Sciences and was organised by myself and a few others.&lt;/p&gt;
&lt;p&gt;The website for the event is hosted &lt;a href=&#34;http://users.cs.cf.ac.uk/W.M.Webberley/digisocial/&#34; target=&#34;_blank&#34; rel=&#34;noopener noreferrer&#34; &gt;here&lt;/a&gt;.&lt;/p&gt;
&lt;p&gt;&lt;img src=&#34;https://wilw.dev/media/blog/digisocial_logo.png&#34; alt=&#34;DigiSocial logo&#34;&gt;&lt;/p&gt;
&lt;p&gt;The idea of the event was to try and encourage further ties between the different Schools of the University. The
University Graduate College (UGC) provide the funding for these events, which must be applied for, in the hope
that good projects or results come out of it.&lt;/p&gt;
&lt;p&gt;We had relatively good responses from the Schools of Maths, Social Sciences, Medicine, and ourselves, and had a turnout of around 10-15
for the event on the 15th and 16th September. Initially, we started to develop ideas for potential projects. Because of the
nature of the event, we wanted to make sure they were as cross-disciplined as possible. A hackday, in itself, is pretty
computer science-y so we needed to apply a social or medical spin on our ideas.&lt;/p&gt;
&lt;p&gt;Eventually, we settled into two groups: one working on a social-themed project based on crimes in an area (both in terms of
distribution and intensity) in relation to the food hygiene levels in nearby establishments; another focusing on hospital wait times
and free beds in South Wales. Effectively, then, both projects are visualisations of publicly-available datasets.&lt;/p&gt;
&lt;p&gt;I worked on the social project with Matt Williams, Wil Chivers and Martin Chorley, and it is viewable &lt;a href=&#34;http://ukcrimemashup.nomovingparts.net/&#34; target=&#34;_blank&#34; rel=&#34;noopener noreferrer&#34; &gt;here&lt;/a&gt;.&lt;/p&gt;
&lt;p&gt;Overall the event was a reasonable success; two projects were
completed and we have now made links with the other Schools which will hopefully allow us to do similar events together in the
future.&lt;/p&gt;
</content:encoded>
    </item>
    
  </channel>
</rss>