<?xml version="1.0" encoding="utf-8"?>
<feed xmlns="http://www.w3.org/2005/Atom">
<title>kmaasrud</title>
<link href="https://kmaasrud.com" />
<link href="https://kmaasrud.com" rel="self" />
<subtitle>thoughts of kmaasrud</subtitle>
<author>
<name>Knut Magnus Aasrud</name>
<email>km@aasrud.com</email>
</author>
<entry>
<title>Optimizing the hell out of my website</title>
<link href="https://kmaasrud.com/blog/optimize.html" />
<id>2C947FEE-8458-4E87-B8DC-E184975412AF</id>
<published>2022-09-26T13:00:00+02:00</published>
<content type="html">&lt;h1 id="optimizing-the-hell-out-of-my-website"&gt;Optimizing the hell out
of my website&lt;/h1&gt;
&lt;!-- atom-id: 2C947FEE-8458-4E87-B8DC-E184975412AF --&gt;
&lt;p&gt;&lt;time datetime="2022-09-26T13:00:00+02:00"&gt;&lt;/p&gt;
&lt;p&gt;I considered my old website to be pretty minimal. It is mostly static
and includes almost no JavaScript, but I wanted to go
&lt;strong&gt;extreme&lt;/strong&gt;.&lt;/p&gt;
&lt;p&gt;&lt;img src="/img/old-website-screenshot.webp" loading="lazy"
alt="Screenshot of my old website." /&gt;&lt;/p&gt;
&lt;p&gt;Previously, my site weighed in at a pretty respectable 1.82 MB. That
is not bad, and the size mostly comes from fonts which are easily
cached. However, in my testing, it did take around 300 ms to load. That
is not optimal IMO, so I want to cut down the number of requests in
addition to the size.&lt;/p&gt;
&lt;h2 id="switching-away-from-hugo"&gt;Switching away from Hugo&lt;/h2&gt;
&lt;p&gt;Hugo has served as my static site generator of choice for a while
now, and it has done it admirably. It does not contribute in any part to
the size of my website (in fact, it actually has handy minimization
techniques built-in.) However, I need greater flexibility in the steps
I’m going to do to reduce my website’s footprint. Additionally, I wanted
a more &lt;em&gt;Unix-y&lt;/em&gt; and minimal feel to my site’s build process.
Therefore, I’m going to make my own system, composed of regular Unix
tools a small self-made program.&lt;/p&gt;
&lt;p&gt;Inspired by &lt;a href="https://mkws.sh/"&gt;mkws&lt;/a&gt;, I created &lt;a
href="https://github.com/kmaasrud/sss"&gt;sss&lt;/a&gt;. It is a small program
that will take a directory of source files, pipe them through a
collection of POSIX shell templates and create some output files. You
can read more about how it works in &lt;a
href="https://github.com/kmaasrud/sss"&gt;it’s README&lt;/a&gt;, but it
essentially just does some clever string interpolation. I was surprised
at how quickly I got my Hugo setup converted to &lt;code&gt;sss&lt;/code&gt;, and
quickly proceeded to size optimization.&lt;/p&gt;
&lt;h2 id="going-small"&gt;Going small&lt;/h2&gt;
&lt;p&gt;These are the steps I took in order to get my page as small as
possible. The sizes reported are on-disk and not gzipped, so they only
give an indication of how much I’ve saved.&lt;/p&gt;
&lt;h3 id="images"&gt;Images&lt;/h3&gt;
&lt;p&gt;Images account for about half the size of the median web site&lt;a
href="#fn1" class="footnote-ref" id="fnref1"
role="doc-noteref"&gt;&lt;sup&gt;1&lt;/sup&gt;&lt;/a&gt;. Simple optimization could reduce
that by quite alot&lt;a href="#fn2" class="footnote-ref" id="fnref2"
role="doc-noteref"&gt;&lt;sup&gt;2&lt;/sup&gt;&lt;/a&gt;. We want to go extreme, which will
involve the following process:&lt;/p&gt;
&lt;ol type="1"&gt;
&lt;li&gt;Start with a PNG file and optimize it with &lt;code&gt;optipng&lt;/code&gt;,
using the highest optimization settings (&lt;code&gt;-o7&lt;/code&gt;.)&lt;/li&gt;
&lt;li&gt;Convert the optimized PNG into WebP with the &lt;code&gt;webp&lt;/code&gt;
tools.&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;I did this with the following simple shell script, to automate the
process for me:&lt;/p&gt;
&lt;div class="sourceCode" id="cb1"&gt;&lt;pre
class="sourceCode bash"&gt;&lt;code class="sourceCode bash"&gt;&lt;span id="cb1-1"&gt;&lt;a href="#cb1-1" aria-hidden="true" tabindex="-1"&gt;&lt;/a&gt;&lt;span class="co"&gt;#!/bin/sh&lt;/span&gt;&lt;/span&gt;
&lt;span id="cb1-2"&gt;&lt;a href="#cb1-2" aria-hidden="true" tabindex="-1"&gt;&lt;/a&gt;&lt;/span&gt;
&lt;span id="cb1-3"&gt;&lt;a href="#cb1-3" aria-hidden="true" tabindex="-1"&gt;&lt;/a&gt;&lt;span class="co"&gt;# Optimize PNG (make backup)&lt;/span&gt;&lt;/span&gt;
&lt;span id="cb1-4"&gt;&lt;a href="#cb1-4" aria-hidden="true" tabindex="-1"&gt;&lt;/a&gt;&lt;span class="ex"&gt;optipng&lt;/span&gt; &lt;span class="at"&gt;--&lt;/span&gt; &lt;span class="at"&gt;-backup&lt;/span&gt; &lt;span class="at"&gt;-o7&lt;/span&gt; &lt;span class="va"&gt;$1&lt;/span&gt;&lt;/span&gt;
&lt;span id="cb1-5"&gt;&lt;a href="#cb1-5" aria-hidden="true" tabindex="-1"&gt;&lt;/a&gt;&lt;/span&gt;
&lt;span id="cb1-6"&gt;&lt;a href="#cb1-6" aria-hidden="true" tabindex="-1"&gt;&lt;/a&gt;&lt;span class="co"&gt;# Convert into WebP&lt;/span&gt;&lt;/span&gt;
&lt;span id="cb1-7"&gt;&lt;a href="#cb1-7" aria-hidden="true" tabindex="-1"&gt;&lt;/a&gt;&lt;span class="ex"&gt;cwebp&lt;/span&gt; &lt;span class="va"&gt;$1&lt;/span&gt; &lt;span class="at"&gt;-q&lt;/span&gt; &lt;span class="va"&gt;$2&lt;/span&gt; &lt;span class="at"&gt;-o&lt;/span&gt; &lt;span class="st"&gt;&amp;quot;&lt;/span&gt;&lt;span class="va"&gt;${1&lt;/span&gt;&lt;span class="op"&gt;%%&lt;/span&gt;.png&lt;span class="va"&gt;}&lt;/span&gt;&lt;span class="st"&gt;.webp&amp;quot;&lt;/span&gt;&lt;/span&gt;
&lt;span id="cb1-8"&gt;&lt;a href="#cb1-8" aria-hidden="true" tabindex="-1"&gt;&lt;/a&gt;&lt;/span&gt;
&lt;span id="cb1-9"&gt;&lt;a href="#cb1-9" aria-hidden="true" tabindex="-1"&gt;&lt;/a&gt;&lt;span class="co"&gt;# Remove original file&lt;/span&gt;&lt;/span&gt;
&lt;span id="cb1-10"&gt;&lt;a href="#cb1-10" aria-hidden="true" tabindex="-1"&gt;&lt;/a&gt;&lt;span class="fu"&gt;rm&lt;/span&gt; &lt;span class="va"&gt;$1&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;
&lt;p&gt;The first parameter is the image file we want to optimize, and the
second is the quality we want to use in the WebP compression (0-100 in
increasing order of quality.)&lt;/p&gt;
&lt;p&gt;&lt;em&gt;The screenshot above had the initial size of &lt;strong&gt;~1.2
MB&lt;/strong&gt;, but the final optimized image ended up being &lt;strong&gt;~22
kB&lt;/strong&gt;.&lt;/em&gt; Quite a drastic improvement, if you ask me.&lt;/p&gt;
&lt;h3 id="katex"&gt;KaTeX&lt;/h3&gt;
&lt;p&gt;Math notation is very important for me, so &lt;a
href="https://katex.org"&gt;KaTeX&lt;/a&gt; is a must. It is by no means big, but
since I’m doing the Markdown rendering with Pandoc - which natively
detects inline and display math - I figured I could get the rendering
done at build time to save having to include the JavaScript library.
This was as easy as using the Pandoc filter &lt;a
href="https://%20github.com/xu-cheng/pandoc-katex"&gt;pandoc-katex&lt;/a&gt;
(which luckily is available in &lt;code&gt;nixpkgs&lt;/code&gt;.)&lt;/p&gt;
&lt;p&gt;From there, I downloaded the KaTeX stylesheet and the corresponding
fonts, in order to let me self-host them and save a few HTTP requests.
We’re living in the modern days now, so I deleted everything except the
&lt;code&gt;woff2&lt;/code&gt; fonts. Additionally, I went over the stylesheet and
removed every redundant rule which I am not in need of (e.g. the
&lt;code&gt;\includegraphics&lt;/code&gt; support.)&lt;/p&gt;
&lt;p&gt;&lt;em&gt;This saved me &lt;strong&gt;~600 kB&lt;/strong&gt; worth of JavaScript and
&lt;strong&gt;~1 MB&lt;/strong&gt; worth of fonts.&lt;/em&gt; However, the fonts don’t
really count, since they are lazily loaded.&lt;/p&gt;
&lt;h3 id="fonts"&gt;Fonts&lt;/h3&gt;
&lt;p&gt;Speaking of fonts, I would have to give up my beloved &lt;a
href="https://rsms.me/inter/"&gt;Inter&lt;/a&gt;. Instead, I’m opting at trusing
the web browser and simply using the default fonts. I’m giving up a bit
of predictability, but it is much better for the end user, as they can
adapt the font choice to their preferences.&lt;/p&gt;
&lt;p&gt;Once again considering the KaTeX fonts, I opted for setting
&lt;code&gt;font-display: swap&lt;/code&gt; for all of the fonts. This instructs the
browser to actually render the markup, even if the fonts have not loaded
yet, instead of blocking. This might lead to a bit of yank, but it
ensures that the &lt;em&gt;content&lt;/em&gt; is prioritized before the layout.&lt;/p&gt;
&lt;p&gt;&lt;em&gt;Removing Inter saved me &lt;strong&gt;~800 kB&lt;/strong&gt;, and I gained a
bit of rendering speed by setting
&lt;code&gt;font-display: swap&lt;/code&gt;.&lt;/em&gt;&lt;/p&gt;
&lt;h3 id="stylesheets"&gt;Stylesheets&lt;/h3&gt;
&lt;p&gt;I want my page to look the same everywhere, so like any good netizen,
I use &lt;a
href="https://necolas.github.io/normalize.css/"&gt;Normalize.css&lt;/a&gt;. To
make it even more minimal, I removed any of the rules I’m never going to
use (like forms and such), and inlined it into my main stylesheet.
Speaking of my main stylesheet, I did as much as I could to keep it as
short as possible, removing any fancy animations or stylings of stuff I
were not planning on using anytime soon.&lt;/p&gt;
&lt;p&gt;The stylesheet is now pretty optimized, so it can’t really be
prioritized enough. For that reason, I decided that I wanted to inline
everything in a &lt;code&gt;&amp;lt;style&amp;gt;&lt;/code&gt; tag for every HTML file
(except KaTeX, for caching reasons). Although this involves a (very
small) penalty on the document size, it is far outweighed by the gains
of not doing that extra GET request.&lt;/p&gt;
&lt;p&gt;Lastly, I stumbled upon &lt;a href="https://lightningcss.dev/"&gt;Lightning
CSS&lt;/a&gt; and opted on using it to minify everything as much as possible.
To me, it seems like it is the best CSS minimizer that’s currently
available in &lt;code&gt;nixpkgs&lt;/code&gt;.&lt;/p&gt;
&lt;p&gt;&lt;em&gt;The minimization saved me &lt;strong&gt;~5 kB&lt;/strong&gt;, and inlining
the normalization and main stylesheet saved me two requests.&lt;/em&gt;&lt;/p&gt;
&lt;h3 id="minimization"&gt;Minimization&lt;/h3&gt;
&lt;p&gt;Last, but not least, we arrive at minimizing the actual HTML file
itself. There are a lot of interesting things which can be done to strip
down byte after byte, like &lt;em&gt;not closing some tags&lt;/em&gt;&lt;a href="#fn3"
class="footnote-ref" id="fnref3" role="doc-noteref"&gt;&lt;sup&gt;3&lt;/sup&gt;&lt;/a&gt;,
&lt;em&gt;stripping out &lt;code&gt;&amp;lt;html&amp;gt;&lt;/code&gt;, &lt;code&gt;&amp;lt;head&amp;gt;&lt;/code&gt;,
&lt;code&gt;&amp;lt;body&amp;gt;&lt;/code&gt;, etc.&lt;/em&gt;&lt;a href="#fn4" class="footnote-ref"
id="fnref4" role="doc-noteref"&gt;&lt;sup&gt;4&lt;/sup&gt;&lt;/a&gt; and much more. This can
quickly get tiresome to do manually, though, which is why we have
minimizers. I found &lt;a
href="https://github.com/wilsonzlin/minify-html"&gt;this one&lt;/a&gt; during my
research, and after some testing, it gave me the best (smallest)
results.&lt;/p&gt;
&lt;p&gt;&lt;em&gt;The pages are quite small to begin with, so HTML minimization
only accounted for &lt;strong&gt;~1 kB&lt;/strong&gt; per page. Every byte counts,
though!&lt;/em&gt;&lt;/p&gt;
&lt;h3 id="favicon"&gt;Favicon&lt;/h3&gt;
&lt;p&gt;One extra step I really wanted to do, which does not contribute a
lot, but is fun nonetheless, is to change the favicon to something
smaller. Since SVG is an XML language which supports CSS and text tags,
you can make an icon that just holds a single emoji&lt;a href="#fn5"
class="footnote-ref" id="fnref5" role="doc-noteref"&gt;&lt;sup&gt;5&lt;/sup&gt;&lt;/a&gt;,
which I presume is the most minimal you can get - unless you want to use
a single square or something as your favicon. Just insert the following
into your head, and exchange the emoji with whatever you prefer:&lt;/p&gt;
&lt;div class="sourceCode" id="cb2"&gt;&lt;pre
class="sourceCode html"&gt;&lt;code class="sourceCode html"&gt;&lt;span id="cb2-1"&gt;&lt;a href="#cb2-1" aria-hidden="true" tabindex="-1"&gt;&lt;/a&gt;&lt;span class="dt"&gt;&amp;lt;&lt;/span&gt;&lt;span class="kw"&gt;link&lt;/span&gt; &lt;span class="er"&gt;rel&lt;/span&gt;&lt;span class="ot"&gt;=&lt;/span&gt;&lt;span class="st"&gt;&amp;quot;icon&amp;quot;&lt;/span&gt; &lt;span class="er"&gt;href&lt;/span&gt;&lt;span class="ot"&gt;=&lt;/span&gt;&lt;span class="st"&gt;&amp;#39;data:image/svg+xml,&lt;/span&gt;&lt;span class="er"&gt;&amp;lt;&lt;/span&gt;&lt;span class="st"&gt;svg xmlns=%22http://www.w3.org/2000/svg%22 viewBox=%220 0 100 100%22&amp;gt;&lt;/span&gt;&lt;span class="er"&gt;&amp;lt;&lt;/span&gt;&lt;span class="st"&gt;text y=%22.9em%22 font-size=%2290%22&amp;gt;💭&lt;/span&gt;&lt;span class="er"&gt;&amp;lt;&lt;/span&gt;&lt;span class="st"&gt;/text&amp;gt;&lt;/span&gt;&lt;span class="er"&gt;&amp;lt;&lt;/span&gt;&lt;span class="st"&gt;/svg&amp;gt;&amp;#39;&lt;/span&gt;&lt;span class="dt"&gt;&amp;gt;&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;
&lt;p&gt;The added advantage here, is that emojis look good on both light and
dark mode, so you don’t need any clever media queries to make the
favicon adapt to the device’s theme.&lt;/p&gt;
&lt;p&gt;&lt;em&gt;The differences here were in the range of single bytes, so I
won’t bother you with any numbers.&lt;/em&gt;&lt;/p&gt;
&lt;h2 id="closing-remarks"&gt;Closing remarks&lt;/h2&gt;
&lt;p&gt;With the incredible networks speeds we currently have, all of this
will likely be considered as unecessary by many. I would however implore
you to think differently about it:&lt;/p&gt;
&lt;p&gt;Take a second to think about the amount of visits bigger newspapers,
social media platforms and the like get every day. Then think about the
amount of data that is shipped to the end user that could be optimized,
is unecessary or never even used. We could save a lot of bandwidth,
computing power and subsequently energy by focusing on cutting out the
cruft. As developers, we should serve as inspiration for this movement
and implement this thinking into our own projects.&lt;/p&gt;
&lt;p&gt;&lt;em&gt;In some sense, we’re saving the world.&lt;/em&gt;&lt;/p&gt;
&lt;aside id="footnotes" class="footnotes footnotes-end-of-document"
role="doc-endnotes"&gt;
&lt;hr /&gt;
&lt;ol&gt;
&lt;li id="fn1"&gt;&lt;p&gt;&lt;a
href="https://httparchive.org/reports/page-weight#bytesImg"
class="uri"&gt;https://httparchive.org/reports/page-weight#bytesImg&lt;/a&gt;&lt;a
href="#fnref1" class="footnote-back" role="doc-backlink"&gt;↩︎&lt;/a&gt;&lt;/p&gt;&lt;/li&gt;
&lt;li id="fn2"&gt;&lt;p&gt;&lt;a
href="https://httparchive.org/reports/state-of-images#optimizedImages"
class="uri"&gt;https://httparchive.org/reports/state-of-images#optimizedImages&lt;/a&gt;&lt;a
href="#fnref2" class="footnote-back" role="doc-backlink"&gt;↩︎&lt;/a&gt;&lt;/p&gt;&lt;/li&gt;
&lt;li id="fn3"&gt;&lt;p&gt;&lt;a
href="https://blog.novalistic.com/archives/2017/08/optional-end-tags-in-html/"
class="uri"&gt;https://blog.novalistic.com/archives/2017/08/optional-end-tags-in-html/&lt;/a&gt;&lt;a
href="#fnref3" class="footnote-back" role="doc-backlink"&gt;↩︎&lt;/a&gt;&lt;/p&gt;&lt;/li&gt;
&lt;li id="fn4"&gt;&lt;p&gt;&lt;a
href="https://endtimes.dev/you-can-leave-out-html-head-and-body-tags/"
class="uri"&gt;https://endtimes.dev/you-can-leave-out-html-head-and-body-tags/&lt;/a&gt;&lt;a
href="#fnref4" class="footnote-back" role="doc-backlink"&gt;↩︎&lt;/a&gt;&lt;/p&gt;&lt;/li&gt;
&lt;li id="fn5"&gt;&lt;p&gt;&lt;a href="https://endtimes.dev/emojis-as-favicons/"
class="uri"&gt;https://endtimes.dev/emojis-as-favicons/&lt;/a&gt;&lt;a
href="#fnref5" class="footnote-back" role="doc-backlink"&gt;↩︎&lt;/a&gt;&lt;/p&gt;&lt;/li&gt;
&lt;/ol&gt;
&lt;/aside&gt;</content>
</entry>
<entry>
<title>OPML is underrated</title>
<link href="https://kmaasrud.com/blog/opml-is-underrated.html" />
<id>a23474ca-0fc0-4444-93ad-21d1d08cded9</id>
<published>2024-02-07T10:52:00+01:00</published>
<content type="html">&lt;h1 id="opml-is-underrated"&gt;OPML is underrated&lt;/h1&gt;
&lt;!-- atom-id: a23474ca-0fc0-4444-93ad-21d1d08cded9 --&gt;
&lt;p&gt;&lt;time datetime="2024-02-07T10:52:00+01:00"&gt;&lt;/p&gt;
&lt;p&gt;As a response to the general &lt;a
href="https://doctorow.medium.com/social-quitting-1ce85b67b456"&gt;enshittification&lt;/a&gt;
of major platforms, I would say we are seeing a resurgence of the old
web’s ethos, with personal blogs gaining traction and the concept of the
&lt;a href="https://neustadt.fr/essays/the-small-web/"&gt;small web&lt;/a&gt; on the
rise. That might be colored by the digital communities I hang around in
(which are mostly dominated by programmers) but it does at least
empirically feel like a trend&lt;a href="#fn1" class="footnote-ref"
id="fnref1" role="doc-noteref"&gt;&lt;sup&gt;1&lt;/sup&gt;&lt;/a&gt;. That brings along with
it a new interest in open web standards. Among them is RSS&lt;a href="#fn2"
class="footnote-ref" id="fnref2" role="doc-noteref"&gt;&lt;sup&gt;2&lt;/sup&gt;&lt;/a&gt;,
which both I, and I think a lot others, have increasingly integrated
into our digital routines to keep track of posts from people and sources
we’re following. RSS aligns perfectly with the movement towards more
personalized and controlled content consumption. Unlike the
algorithm-driven feeds of &lt;q&gt;other platforms&lt;/q&gt; — which often
prioritize engagement over relevance or quality — RSS allows me to
curate my own information stream. This feels important to me, as it
gives me a level of autonomy over the content that shapes my views and
knowledge, as opposed to handing that power over to advertisers.&lt;/p&gt;
&lt;p&gt;There is an issue, though. RSS feeds can be a bit clunky to manage
and keep track of. Their decentralized nature also makes discoverability
an issue. Enter &lt;a href="http://opml.org/spec2.opml"&gt;OPML&lt;/a&gt;, which is
an outliner format that is most commonly used to store &lt;a
href="http://opml.org/spec2.opml#subscriptionLists"&gt;a list of feed
subscriptions&lt;/a&gt;. I promise you; having a single file that stores all
the feeds you’re interested in is a gamechanger, as it makes it
significantly easier to organize, migrate, and share those feeds across
different platforms and devices. Here’s an example:&lt;/p&gt;
&lt;div class="sourceCode" id="cb1"&gt;&lt;pre
class="sourceCode xml"&gt;&lt;code class="sourceCode xml"&gt;&lt;span id="cb1-1"&gt;&lt;a href="#cb1-1" aria-hidden="true" tabindex="-1"&gt;&lt;/a&gt;&lt;span class="fu"&gt;&amp;lt;?xml&lt;/span&gt;&lt;span class="ot"&gt; version=&lt;/span&gt;&lt;span class="st"&gt;&amp;quot;1.0&amp;quot;&lt;/span&gt;&lt;span class="ot"&gt; encoding=&lt;/span&gt;&lt;span class="st"&gt;&amp;quot;utf-8&amp;quot;&lt;/span&gt;&lt;span class="fu"&gt;?&amp;gt;&lt;/span&gt;&lt;/span&gt;
&lt;span id="cb1-2"&gt;&lt;a href="#cb1-2" aria-hidden="true" tabindex="-1"&gt;&lt;/a&gt;&amp;lt;&lt;span class="kw"&gt;opml&lt;/span&gt;&lt;span class="ot"&gt; version=&lt;/span&gt;&lt;span class="st"&gt;&amp;quot;2.0&amp;quot;&lt;/span&gt;&amp;gt;&lt;/span&gt;
&lt;span id="cb1-3"&gt;&lt;a href="#cb1-3" aria-hidden="true" tabindex="-1"&gt;&lt;/a&gt;  &amp;lt;&lt;span class="kw"&gt;head&lt;/span&gt;&amp;gt;&lt;/span&gt;
&lt;span id="cb1-4"&gt;&lt;a href="#cb1-4" aria-hidden="true" tabindex="-1"&gt;&lt;/a&gt;    &amp;lt;&lt;span class="kw"&gt;title&lt;/span&gt;&amp;gt;A list of feeds I follow&amp;lt;/&lt;span class="kw"&gt;title&lt;/span&gt;&amp;gt;&lt;/span&gt;
&lt;span id="cb1-5"&gt;&lt;a href="#cb1-5" aria-hidden="true" tabindex="-1"&gt;&lt;/a&gt;  &amp;lt;/&lt;span class="kw"&gt;head&lt;/span&gt;&amp;gt;&lt;/span&gt;
&lt;span id="cb1-6"&gt;&lt;a href="#cb1-6" aria-hidden="true" tabindex="-1"&gt;&lt;/a&gt;  &amp;lt;&lt;span class="kw"&gt;body&lt;/span&gt;&amp;gt;&lt;/span&gt;
&lt;span id="cb1-7"&gt;&lt;a href="#cb1-7" aria-hidden="true" tabindex="-1"&gt;&lt;/a&gt;    &amp;lt;&lt;span class="kw"&gt;outline&lt;/span&gt;&lt;span class="ot"&gt; text=&lt;/span&gt;&lt;span class="st"&gt;&amp;quot;My favorite blog&amp;quot;&lt;/span&gt;&lt;span class="ot"&gt; xmlUrl=&lt;/span&gt;&lt;span class="st"&gt;&amp;quot;https://a-cool-blog.tld/blog/feed.xml&amp;quot;&lt;/span&gt;&lt;span class="ot"&gt; type=&lt;/span&gt;&lt;span class="st"&gt;&amp;quot;rss&amp;quot;&lt;/span&gt;&lt;/span&gt;
&lt;span id="cb1-8"&gt;&lt;a href="#cb1-8" aria-hidden="true" tabindex="-1"&gt;&lt;/a&gt;&lt;span class="ot"&gt;      htmlUrl=&lt;/span&gt;&lt;span class="st"&gt;&amp;quot;https://a-cool-blog.tld/blog&amp;quot;&lt;/span&gt;&lt;span class="ot"&gt; description=&lt;/span&gt;&lt;span class="st"&gt;&amp;quot;You can also add a description&amp;quot;&lt;/span&gt; /&amp;gt;&lt;/span&gt;
&lt;span id="cb1-9"&gt;&lt;a href="#cb1-9" aria-hidden="true" tabindex="-1"&gt;&lt;/a&gt;    &lt;span class="co"&gt;&amp;lt;!-- more outline elements with feeds... --&amp;gt;&lt;/span&gt;&lt;/span&gt;
&lt;span id="cb1-10"&gt;&lt;a href="#cb1-10" aria-hidden="true" tabindex="-1"&gt;&lt;/a&gt;  &amp;lt;/&lt;span class="kw"&gt;body&lt;/span&gt;&amp;gt;&lt;/span&gt;
&lt;span id="cb1-11"&gt;&lt;a href="#cb1-11" aria-hidden="true" tabindex="-1"&gt;&lt;/a&gt;&amp;lt;/&lt;span class="kw"&gt;opml&lt;/span&gt;&amp;gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;
&lt;p&gt;Each outline item must have the type &lt;code&gt;rss&lt;/code&gt; (that goes for
both RSS and Atom feeds) and must include the &lt;code&gt;xmlUrl&lt;/code&gt;
attribute. Optionally, you can specify some more attributes, like adding
a title with &lt;code&gt;text&lt;/code&gt;, a description with
&lt;code&gt;description&lt;/code&gt; and a link to the blog front page with
&lt;code&gt;htmlUrl&lt;/code&gt; — that added metadata can be very useful. Yes, it
is XML-based, which I admit isn’t exactly the easiest format to work
with, but it has a few advantages, which we’ll get back to.&lt;/p&gt;
&lt;p&gt;With OPML, you don’t need separate applications or services to
categorize feeds. Categorization can be achieved within a single OPML
file through its outlining capabilities or by managing multiple OPML
files, each dedicated to a different category or use-case. It is a very
viable workflow to have one OPML file for your YouTube subscriptions,
another for your favorite Twitter/X and Mastodon users, one more for
news sites, and yet another for personal blogs — the world’s your
oyster. However, there aren’t many application that support nested OPML
outlines or categorizing based on different files, sadly, but there
should be! This is a call to action, developers: Perfect
side-project!&lt;/p&gt;
&lt;p&gt;Beyond personal convenience, OPML has the potential to better the
&lt;em&gt;ecosystem&lt;/em&gt; of the &lt;q&gt;small web.&lt;/q&gt; By not only sharing an RSS
feed on your personal website, but also your list of subscribed feeds,
we’re effectively creating a recommendation system that is based on
concious curation, not statistical metrics. Your OPML file is now called
a &lt;em&gt;&lt;a
href="https://blogroll.org/what-are-blogrolls/"&gt;blogroll&lt;/a&gt;&lt;/em&gt;, and
you officially get to call yourself a &lt;strong&gt;90s web
developer&lt;/strong&gt;. Jokes aside, I believe the simple fact that there is
a known person behind each recommendation is advantageous. Yes, this
might promote smaller digital social circles, but I personally think the
transparency of a known source is the best way to combat &lt;a
href="https://en.wikipedia.org/wiki/Filter_bubble"&gt;filter bubbles&lt;/a&gt;.
That part is a whole sociological discussion in itself, so if you would
like to discuss it further, I would love chatting about it on &lt;a
href="https://lists.sr.ht/~kmaasrud/inbox"&gt;my mailing list&lt;/a&gt;.&lt;/p&gt;
&lt;p&gt;Now, getting back to the fact that OPML is XML-based; I’d like to
highlight an often-overlooked feature of this: The ability to use an XSL
stylesheet to display the OPML file rendered through a HTML template
when loaded in a browser. With this, you can add a short introduction
and guide to the format, making the blogroll more accessible to those
unfamiliar with it. It also opens the possibility to showcase each feed
with added context or descriptions.&lt;/p&gt;
&lt;details&gt;
&lt;summary&gt;
Here is an example XSL stylesheet you can use:
&lt;/summary&gt;
&lt;div class="sourceCode" id="cb2"&gt;&lt;pre
class="sourceCode xsl"&gt;&lt;code class="sourceCode xslt"&gt;&lt;span id="cb2-1"&gt;&lt;a href="#cb2-1" aria-hidden="true" tabindex="-1"&gt;&lt;/a&gt;&lt;span class="fu"&gt;&amp;lt;?&lt;/span&gt;&lt;span class="kw"&gt;xml&lt;/span&gt;&lt;span class="ot"&gt; version=&lt;/span&gt;&lt;span class="st"&gt;&amp;quot;1.0&amp;quot;&lt;/span&gt;&lt;span class="ot"&gt; encoding=&lt;/span&gt;&lt;span class="st"&gt;&amp;quot;UTF-8&amp;quot;&lt;/span&gt;&lt;span class="fu"&gt;?&amp;gt;&lt;/span&gt;&lt;/span&gt;
&lt;span id="cb2-2"&gt;&lt;a href="#cb2-2" aria-hidden="true" tabindex="-1"&gt;&lt;/a&gt;&lt;span class="kw"&gt;&amp;lt;&lt;/span&gt;&lt;span class="bu"&gt;xsl:stylesheet&lt;/span&gt;&lt;span class="ot"&gt; version=&lt;/span&gt;&lt;span class="st"&gt;&amp;quot;1.0&amp;quot;&lt;/span&gt;&lt;span class="ot"&gt; xmlns:xsl=&lt;/span&gt;&lt;span class="st"&gt;&amp;quot;http://www.w3.org/1999/XSL/Transform&amp;quot;&lt;/span&gt;&lt;span class="kw"&gt;&amp;gt;&lt;/span&gt;&lt;/span&gt;
&lt;span id="cb2-3"&gt;&lt;a href="#cb2-3" aria-hidden="true" tabindex="-1"&gt;&lt;/a&gt;  &lt;span class="kw"&gt;&amp;lt;&lt;/span&gt;&lt;span class="bu"&gt;xsl:template&lt;/span&gt;&lt;span class="ot"&gt; match=&lt;/span&gt;&lt;span class="va"&gt;&amp;quot;/opml&amp;quot;&lt;/span&gt;&lt;span class="kw"&gt;&amp;gt;&lt;/span&gt;&lt;/span&gt;
&lt;span id="cb2-4"&gt;&lt;a href="#cb2-4" aria-hidden="true" tabindex="-1"&gt;&lt;/a&gt;    &lt;span class="kw"&gt;&amp;lt;html&lt;/span&gt;&lt;span class="ot"&gt; xmlns=&lt;/span&gt;&lt;span class="st"&gt;&amp;quot;http://www.w3.org/1999/xhtml&amp;quot;&lt;/span&gt;&lt;span class="ot"&gt; xml:lang=&lt;/span&gt;&lt;span class="st"&gt;&amp;quot;en&amp;quot;&lt;/span&gt;&lt;span class="ot"&gt; lang=&lt;/span&gt;&lt;span class="st"&gt;&amp;quot;en&amp;quot;&lt;/span&gt;&lt;span class="kw"&gt;&amp;gt;&lt;/span&gt;&lt;/span&gt;
&lt;span id="cb2-5"&gt;&lt;a href="#cb2-5" aria-hidden="true" tabindex="-1"&gt;&lt;/a&gt;      &lt;span class="kw"&gt;&amp;lt;head&amp;gt;&lt;/span&gt;&lt;/span&gt;
&lt;span id="cb2-6"&gt;&lt;a href="#cb2-6" aria-hidden="true" tabindex="-1"&gt;&lt;/a&gt;        &lt;span class="kw"&gt;&amp;lt;title&amp;gt;&lt;/span&gt;&lt;/span&gt;
&lt;span id="cb2-7"&gt;&lt;a href="#cb2-7" aria-hidden="true" tabindex="-1"&gt;&lt;/a&gt;          &lt;span class="kw"&gt;&amp;lt;&lt;/span&gt;&lt;span class="bu"&gt;xsl:value-of&lt;/span&gt;&lt;span class="ot"&gt; select=&lt;/span&gt;&lt;span class="va"&gt;&amp;quot;head/title&amp;quot;&lt;/span&gt;&lt;span class="kw"&gt;/&amp;gt;&lt;/span&gt;&lt;/span&gt;
&lt;span id="cb2-8"&gt;&lt;a href="#cb2-8" aria-hidden="true" tabindex="-1"&gt;&lt;/a&gt;        &lt;span class="kw"&gt;&amp;lt;/title&amp;gt;&lt;/span&gt;&lt;/span&gt;
&lt;span id="cb2-9"&gt;&lt;a href="#cb2-9" aria-hidden="true" tabindex="-1"&gt;&lt;/a&gt;        &lt;span class="kw"&gt;&amp;lt;meta&lt;/span&gt;&lt;span class="ot"&gt; name=&lt;/span&gt;&lt;span class="st"&gt;&amp;quot;viewport&amp;quot;&lt;/span&gt;&lt;span class="ot"&gt; content=&lt;/span&gt;&lt;span class="st"&gt;&amp;quot;width=device-width, initial-scale=1&amp;quot;&lt;/span&gt;&lt;span class="kw"&gt;/&amp;gt;&lt;/span&gt;&lt;/span&gt;
&lt;span id="cb2-10"&gt;&lt;a href="#cb2-10" aria-hidden="true" tabindex="-1"&gt;&lt;/a&gt;        &lt;span class="kw"&gt;&amp;lt;style&amp;gt;&lt;/span&gt; /* Insert CSS here */ &lt;span class="kw"&gt;&amp;lt;/style&amp;gt;&lt;/span&gt;&lt;/span&gt;
&lt;span id="cb2-11"&gt;&lt;a href="#cb2-11" aria-hidden="true" tabindex="-1"&gt;&lt;/a&gt;      &lt;span class="kw"&gt;&amp;lt;/head&amp;gt;&lt;/span&gt;&lt;/span&gt;
&lt;span id="cb2-12"&gt;&lt;a href="#cb2-12" aria-hidden="true" tabindex="-1"&gt;&lt;/a&gt;      &lt;span class="kw"&gt;&amp;lt;body&amp;gt;&lt;/span&gt;&lt;/span&gt;
&lt;span id="cb2-13"&gt;&lt;a href="#cb2-13" aria-hidden="true" tabindex="-1"&gt;&lt;/a&gt;        &lt;span class="kw"&gt;&amp;lt;p&amp;gt;&lt;/span&gt;&lt;/span&gt;
&lt;span id="cb2-14"&gt;&lt;a href="#cb2-14" aria-hidden="true" tabindex="-1"&gt;&lt;/a&gt;          This is a list of blogs and news sources I follow. The page&lt;/span&gt;
&lt;span id="cb2-15"&gt;&lt;a href="#cb2-15" aria-hidden="true" tabindex="-1"&gt;&lt;/a&gt;          is itself an &lt;span class="kw"&gt;&amp;lt;a&lt;/span&gt;&lt;span class="ot"&gt; href=&lt;/span&gt;&lt;span class="st"&gt;&amp;quot;http://opml.org/&amp;quot;&lt;/span&gt;&lt;span class="kw"&gt;&amp;gt;&lt;/span&gt;OPML&lt;span class="kw"&gt;&amp;lt;/a&amp;gt;&lt;/span&gt; file, which&lt;/span&gt;
&lt;span id="cb2-16"&gt;&lt;a href="#cb2-16" aria-hidden="true" tabindex="-1"&gt;&lt;/a&gt;          means you can copy the link into your RSS reader to&lt;/span&gt;
&lt;span id="cb2-17"&gt;&lt;a href="#cb2-17" aria-hidden="true" tabindex="-1"&gt;&lt;/a&gt;          subscribe to all the feeds listed below.&lt;/span&gt;
&lt;span id="cb2-18"&gt;&lt;a href="#cb2-18" aria-hidden="true" tabindex="-1"&gt;&lt;/a&gt;        &lt;span class="kw"&gt;&amp;lt;/p&amp;gt;&lt;/span&gt;&lt;/span&gt;
&lt;span id="cb2-19"&gt;&lt;a href="#cb2-19" aria-hidden="true" tabindex="-1"&gt;&lt;/a&gt;        &lt;span class="kw"&gt;&amp;lt;ul&amp;gt;&lt;/span&gt;&lt;/span&gt;
&lt;span id="cb2-20"&gt;&lt;a href="#cb2-20" aria-hidden="true" tabindex="-1"&gt;&lt;/a&gt;          &lt;span class="kw"&gt;&amp;lt;&lt;/span&gt;&lt;span class="bu"&gt;xsl:apply-templates&lt;/span&gt;&lt;span class="ot"&gt; select=&lt;/span&gt;&lt;span class="va"&gt;&amp;quot;body/outline&amp;quot;&lt;/span&gt;&lt;span class="kw"&gt;/&amp;gt;&lt;/span&gt;&lt;/span&gt;
&lt;span id="cb2-21"&gt;&lt;a href="#cb2-21" aria-hidden="true" tabindex="-1"&gt;&lt;/a&gt;        &lt;span class="kw"&gt;&amp;lt;/ul&amp;gt;&lt;/span&gt;&lt;/span&gt;
&lt;span id="cb2-22"&gt;&lt;a href="#cb2-22" aria-hidden="true" tabindex="-1"&gt;&lt;/a&gt;      &lt;span class="kw"&gt;&amp;lt;/body&amp;gt;&lt;/span&gt;&lt;/span&gt;
&lt;span id="cb2-23"&gt;&lt;a href="#cb2-23" aria-hidden="true" tabindex="-1"&gt;&lt;/a&gt;    &lt;span class="kw"&gt;&amp;lt;/html&amp;gt;&lt;/span&gt;&lt;/span&gt;
&lt;span id="cb2-24"&gt;&lt;a href="#cb2-24" aria-hidden="true" tabindex="-1"&gt;&lt;/a&gt;  &lt;span class="kw"&gt;&amp;lt;/&lt;/span&gt;&lt;span class="bu"&gt;xsl:template&lt;/span&gt;&lt;span class="kw"&gt;&amp;gt;&lt;/span&gt;&lt;/span&gt;
&lt;span id="cb2-25"&gt;&lt;a href="#cb2-25" aria-hidden="true" tabindex="-1"&gt;&lt;/a&gt;  &lt;span class="kw"&gt;&amp;lt;&lt;/span&gt;&lt;span class="bu"&gt;xsl:template&lt;/span&gt;&lt;span class="ot"&gt; match=&lt;/span&gt;&lt;span class="va"&gt;&amp;quot;outline&amp;quot;&lt;/span&gt;&lt;span class="ot"&gt; xmlns=&lt;/span&gt;&lt;span class="st"&gt;&amp;quot;http://www.w3.org/1999/xhtml&amp;quot;&lt;/span&gt;&lt;span class="kw"&gt;&amp;gt;&lt;/span&gt;&lt;/span&gt;
&lt;span id="cb2-26"&gt;&lt;a href="#cb2-26" aria-hidden="true" tabindex="-1"&gt;&lt;/a&gt;    &lt;span class="kw"&gt;&amp;lt;&lt;/span&gt;&lt;span class="bu"&gt;xsl:choose&lt;/span&gt;&lt;span class="kw"&gt;&amp;gt;&lt;/span&gt;&lt;/span&gt;
&lt;span id="cb2-27"&gt;&lt;a href="#cb2-27" aria-hidden="true" tabindex="-1"&gt;&lt;/a&gt;      &lt;span class="kw"&gt;&amp;lt;&lt;/span&gt;&lt;span class="bu"&gt;xsl:when&lt;/span&gt;&lt;span class="ot"&gt; test=&lt;/span&gt;&lt;span class="va"&gt;&amp;quot;@type&amp;quot;&lt;/span&gt;&lt;span class="kw"&gt;&amp;gt;&lt;/span&gt;&lt;/span&gt;
&lt;span id="cb2-28"&gt;&lt;a href="#cb2-28" aria-hidden="true" tabindex="-1"&gt;&lt;/a&gt;        &lt;span class="kw"&gt;&amp;lt;&lt;/span&gt;&lt;span class="bu"&gt;xsl:choose&lt;/span&gt;&lt;span class="kw"&gt;&amp;gt;&lt;/span&gt;&lt;/span&gt;
&lt;span id="cb2-29"&gt;&lt;a href="#cb2-29" aria-hidden="true" tabindex="-1"&gt;&lt;/a&gt;          &lt;span class="kw"&gt;&amp;lt;&lt;/span&gt;&lt;span class="bu"&gt;xsl:when&lt;/span&gt;&lt;span class="ot"&gt; test=&lt;/span&gt;&lt;span class="va"&gt;&amp;quot;@xmlUrl&amp;quot;&lt;/span&gt;&lt;span class="kw"&gt;&amp;gt;&lt;/span&gt;&lt;/span&gt;
&lt;span id="cb2-30"&gt;&lt;a href="#cb2-30" aria-hidden="true" tabindex="-1"&gt;&lt;/a&gt;            &lt;span class="kw"&gt;&amp;lt;li&amp;gt;&lt;/span&gt;&lt;/span&gt;
&lt;span id="cb2-31"&gt;&lt;a href="#cb2-31" aria-hidden="true" tabindex="-1"&gt;&lt;/a&gt;              &lt;span class="kw"&gt;&amp;lt;strong&amp;gt;&lt;/span&gt;&lt;/span&gt;
&lt;span id="cb2-32"&gt;&lt;a href="#cb2-32" aria-hidden="true" tabindex="-1"&gt;&lt;/a&gt;                &lt;span class="kw"&gt;&amp;lt;a&lt;/span&gt;&lt;span class="ot"&gt; href=&lt;/span&gt;&lt;span class="st"&gt;&amp;quot;&lt;/span&gt;&lt;span class="va"&gt;{@htmlUrl}&lt;/span&gt;&lt;span class="st"&gt;&amp;quot;&lt;/span&gt;&lt;span class="kw"&gt;&amp;gt;&amp;lt;&lt;/span&gt;&lt;span class="bu"&gt;xsl:value-of&lt;/span&gt;&lt;span class="ot"&gt; select=&lt;/span&gt;&lt;span class="va"&gt;&amp;quot;@text&amp;quot;&lt;/span&gt;&lt;span class="kw"&gt;/&amp;gt;&amp;lt;/a&amp;gt;&lt;/span&gt;&lt;/span&gt;
&lt;span id="cb2-33"&gt;&lt;a href="#cb2-33" aria-hidden="true" tabindex="-1"&gt;&lt;/a&gt;                (&lt;span class="kw"&gt;&amp;lt;a&lt;/span&gt;&lt;span class="ot"&gt; class=&lt;/span&gt;&lt;span class="st"&gt;&amp;quot;feed&amp;quot;&lt;/span&gt;&lt;span class="ot"&gt; href=&lt;/span&gt;&lt;span class="st"&gt;&amp;quot;&lt;/span&gt;&lt;span class="va"&gt;{@xmlUrl}&lt;/span&gt;&lt;span class="st"&gt;&amp;quot;&lt;/span&gt;&lt;span class="kw"&gt;&amp;gt;&lt;/span&gt;feed&lt;span class="kw"&gt;&amp;lt;/a&amp;gt;&lt;/span&gt;)&lt;/span&gt;
&lt;span id="cb2-34"&gt;&lt;a href="#cb2-34" aria-hidden="true" tabindex="-1"&gt;&lt;/a&gt;              &lt;span class="kw"&gt;&amp;lt;/strong&amp;gt;&lt;/span&gt;&lt;/span&gt;
&lt;span id="cb2-35"&gt;&lt;a href="#cb2-35" aria-hidden="true" tabindex="-1"&gt;&lt;/a&gt;              &lt;span class="kw"&gt;&amp;lt;&lt;/span&gt;&lt;span class="bu"&gt;xsl:choose&lt;/span&gt;&lt;span class="kw"&gt;&amp;gt;&lt;/span&gt;&lt;/span&gt;
&lt;span id="cb2-36"&gt;&lt;a href="#cb2-36" aria-hidden="true" tabindex="-1"&gt;&lt;/a&gt;                &lt;span class="kw"&gt;&amp;lt;&lt;/span&gt;&lt;span class="bu"&gt;xsl:when&lt;/span&gt;&lt;span class="ot"&gt; test=&lt;/span&gt;&lt;span class="va"&gt;&amp;quot;@description != &lt;/span&gt;&lt;span class="st"&gt;&amp;#39;&amp;#39;&lt;/span&gt;&lt;span class="va"&gt;&amp;quot;&lt;/span&gt;&lt;span class="kw"&gt;&amp;gt;&lt;/span&gt;&lt;/span&gt;
&lt;span id="cb2-37"&gt;&lt;a href="#cb2-37" aria-hidden="true" tabindex="-1"&gt;&lt;/a&gt;                  &lt;span class="kw"&gt;&amp;lt;br/&amp;gt;&amp;lt;&lt;/span&gt;&lt;span class="bu"&gt;xsl:value-of&lt;/span&gt;&lt;span class="ot"&gt; select=&lt;/span&gt;&lt;span class="va"&gt;&amp;quot;@description&amp;quot;&lt;/span&gt;&lt;span class="kw"&gt;/&amp;gt;&lt;/span&gt;&lt;/span&gt;
&lt;span id="cb2-38"&gt;&lt;a href="#cb2-38" aria-hidden="true" tabindex="-1"&gt;&lt;/a&gt;                &lt;span class="kw"&gt;&amp;lt;/&lt;/span&gt;&lt;span class="bu"&gt;xsl:when&lt;/span&gt;&lt;span class="kw"&gt;&amp;gt;&lt;/span&gt;&lt;/span&gt;
&lt;span id="cb2-39"&gt;&lt;a href="#cb2-39" aria-hidden="true" tabindex="-1"&gt;&lt;/a&gt;              &lt;span class="kw"&gt;&amp;lt;/&lt;/span&gt;&lt;span class="bu"&gt;xsl:choose&lt;/span&gt;&lt;span class="kw"&gt;&amp;gt;&lt;/span&gt;&lt;/span&gt;
&lt;span id="cb2-40"&gt;&lt;a href="#cb2-40" aria-hidden="true" tabindex="-1"&gt;&lt;/a&gt;            &lt;span class="kw"&gt;&amp;lt;/li&amp;gt;&lt;/span&gt;&lt;/span&gt;
&lt;span id="cb2-41"&gt;&lt;a href="#cb2-41" aria-hidden="true" tabindex="-1"&gt;&lt;/a&gt;          &lt;span class="kw"&gt;&amp;lt;/&lt;/span&gt;&lt;span class="bu"&gt;xsl:when&lt;/span&gt;&lt;span class="kw"&gt;&amp;gt;&lt;/span&gt;&lt;/span&gt;
&lt;span id="cb2-42"&gt;&lt;a href="#cb2-42" aria-hidden="true" tabindex="-1"&gt;&lt;/a&gt;        &lt;span class="kw"&gt;&amp;lt;/&lt;/span&gt;&lt;span class="bu"&gt;xsl:choose&lt;/span&gt;&lt;span class="kw"&gt;&amp;gt;&lt;/span&gt;&lt;/span&gt;
&lt;span id="cb2-43"&gt;&lt;a href="#cb2-43" aria-hidden="true" tabindex="-1"&gt;&lt;/a&gt;      &lt;span class="kw"&gt;&amp;lt;/&lt;/span&gt;&lt;span class="bu"&gt;xsl:when&lt;/span&gt;&lt;span class="kw"&gt;&amp;gt;&lt;/span&gt;&lt;/span&gt;
&lt;span id="cb2-44"&gt;&lt;a href="#cb2-44" aria-hidden="true" tabindex="-1"&gt;&lt;/a&gt;    &lt;span class="kw"&gt;&amp;lt;/&lt;/span&gt;&lt;span class="bu"&gt;xsl:choose&lt;/span&gt;&lt;span class="kw"&gt;&amp;gt;&lt;/span&gt;&lt;/span&gt;
&lt;span id="cb2-45"&gt;&lt;a href="#cb2-45" aria-hidden="true" tabindex="-1"&gt;&lt;/a&gt;  &lt;span class="kw"&gt;&amp;lt;/&lt;/span&gt;&lt;span class="bu"&gt;xsl:template&lt;/span&gt;&lt;span class="kw"&gt;&amp;gt;&lt;/span&gt;&lt;/span&gt;
&lt;span id="cb2-46"&gt;&lt;a href="#cb2-46" aria-hidden="true" tabindex="-1"&gt;&lt;/a&gt;&lt;span class="kw"&gt;&amp;lt;/&lt;/span&gt;&lt;span class="bu"&gt;xsl:stylesheet&lt;/span&gt;&lt;span class="kw"&gt;&amp;gt;&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;
&lt;/details&gt;
&lt;p&gt;You can link to the stylesheet in your OPML file by adding
&lt;code&gt;&amp;lt;?xml-stylesheet type="text/xsl" href="path/to/stylesheet.xsl"?&amp;gt;&lt;/code&gt;
at the top. I actually do this on &lt;a
href="https://kmaasrud.com/blogroll.xml"&gt;my own blogroll&lt;/a&gt;, so check
that out if you want some inspiration.&lt;/p&gt;
&lt;p&gt;While we’re all getting a bit fed up with the big platforms, OPML is
like a breath of fresh air from the old web days. It’s all about making
life easier when managing feeds, sharing cool finds, and stumbling upon
new stuff. So I encourage you to create your own blogroll, slap it on
your website, and share what you’re into. It’s a simple move, but it
could spark some real connections and bring back a bit of that community
vibe we miss.&lt;/p&gt;
&lt;aside id="footnotes" class="footnotes footnotes-end-of-document"
role="doc-endnotes"&gt;
&lt;hr /&gt;
&lt;ol&gt;
&lt;li id="fn1"&gt;&lt;p&gt;Please reach out to me if you have some hard time-series
statistics on the number of people with active personal websites.&lt;a
href="#fnref1" class="footnote-back" role="doc-backlink"&gt;↩︎&lt;/a&gt;&lt;/p&gt;&lt;/li&gt;
&lt;li id="fn2"&gt;&lt;p&gt;Like most people, I’ll be using &lt;q&gt;RSS&lt;/q&gt; to refer to
both RSS &lt;em&gt;and&lt;/em&gt; Atom. You can read more about the differences
between the two &lt;a
href="https://en.wikipedia.org/wiki/RSS#RSS_compared_with_Atom"&gt;here&lt;/a&gt;.&lt;a
href="#fnref2" class="footnote-back" role="doc-backlink"&gt;↩︎&lt;/a&gt;&lt;/p&gt;&lt;/li&gt;
&lt;/ol&gt;
&lt;/aside&gt;</content>
</entry>
<entry>
<title>My experience with Nix</title>
<link href="https://kmaasrud.com/blog/nix.html" />
<id>F06F96D2-7ECF-4C7D-AADE-9D2CBA4B3127</id>
<published>2022-05-16T13:00:00+02:00</published>
<content type="html">&lt;h1 id="my-experience-with-nix"&gt;My experience with Nix&lt;/h1&gt;
&lt;!-- atom-id: F06F96D2-7ECF-4C7D-AADE-9D2CBA4B3127 --&gt;
&lt;p&gt;&lt;time datetime="2022-05-16T13:00:00+02:00"&gt;&lt;/p&gt;
&lt;p&gt;At the beginning of this year, I had read a lot about &lt;a
href="https://nixos.org"&gt;Nix&lt;/a&gt; and felt pretty confident that this
wasn’t something for me. That was, until I did a voicecall with my good
friend &lt;a href="https://glorifiedgluer.com"&gt;Victor&lt;/a&gt;. He was showing
me these really neat tricks on how I could use Nix and Nix Flakes to
handle configuration files, build software reproducibly and define my
own development shell for each project I’m doing - I was blown away.&lt;/p&gt;
&lt;p&gt;After that call, I immediately started with the (admittedly stupid)
thing that every developer does when learning about a new technology. I
started moving all my shit over to it: System configuration, package
&lt;em&gt;management&lt;/em&gt;, package &lt;em&gt;configuration&lt;/em&gt;, etc. - I wanted it
all reproducible. In this blog post I will document my experience so
far. This is very far from a guide to using Nix, but you might find some
useful hints from my mistakes.&lt;/p&gt;
&lt;h2 id="installing-nix"&gt;Installing Nix&lt;/h2&gt;
&lt;p&gt;Okay, the first step was to &lt;em&gt;aquire&lt;/em&gt; Nix. I use a &lt;a
href="https://en.wikipedia.org/wiki/MacBook_Air_(Apple_silicon)"&gt;Macbook
Air M1&lt;/a&gt;, and I quickly discovered that Apple have made this a hastle.
After Catalina, &lt;a href="https://support.apple.com/en-us/HT210650"&gt;you
are not able to write to the root directory of Macs&lt;/a&gt;, which meant
getting the &lt;code&gt;/nix&lt;/code&gt; directory up and running required hacky
workarounds. Luckily, these are now intergrated into the installer,
which made the install very simple.&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;$ sh &amp;lt;(curl -L https://nixos.org/nix/install)&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;did the trick for me. A reboot was needed, but then everything was up
and running smoothly. &lt;strong&gt;Nice!&lt;/strong&gt;&lt;/p&gt;
&lt;h2 id="setting-up-nix-darwin"&gt;Setting up nix-darwin&lt;/h2&gt;
&lt;p&gt;The next step was to get my system built through Nix. This is a
pretty wacky concept, but the advantages are incredible. Think of all
the times you’ve bought a new computer or needed to do a factory reset.
All those preferences you’ve tinkered with to perfection over the years:
They do not need to be set up again! A &lt;code&gt;git clone&lt;/code&gt;, one
simple command, most likely a few minutes of building and you have all
your apps and software up and running.&lt;/p&gt;
&lt;p&gt;I started with &lt;a
href="https://github.com/LnL7/nix-darwin#flakes-experimental"&gt;this
guide&lt;/a&gt; and tinkered a bit according to my system:&lt;/p&gt;
&lt;div class="sourceCode" id="cb2"&gt;&lt;pre
class="sourceCode nix"&gt;&lt;code class="sourceCode nix"&gt;&lt;span id="cb2-1"&gt;&lt;a href="#cb2-1" aria-hidden="true" tabindex="-1"&gt;&lt;/a&gt;&lt;span class="op"&gt;{&lt;/span&gt;&lt;/span&gt;
&lt;span id="cb2-2"&gt;&lt;a href="#cb2-2" aria-hidden="true" tabindex="-1"&gt;&lt;/a&gt;  &lt;span class="va"&gt;description&lt;/span&gt; &lt;span class="op"&gt;=&lt;/span&gt; &lt;span class="st"&gt;&amp;quot;kmaasrud&amp;#39;s system&amp;quot;&lt;/span&gt;&lt;span class="op"&gt;;&lt;/span&gt;&lt;/span&gt;
&lt;span id="cb2-3"&gt;&lt;a href="#cb2-3" aria-hidden="true" tabindex="-1"&gt;&lt;/a&gt;&lt;/span&gt;
&lt;span id="cb2-4"&gt;&lt;a href="#cb2-4" aria-hidden="true" tabindex="-1"&gt;&lt;/a&gt;  &lt;span class="va"&gt;inputs&lt;/span&gt; &lt;span class="op"&gt;=&lt;/span&gt; &lt;span class="op"&gt;{&lt;/span&gt;&lt;/span&gt;
&lt;span id="cb2-5"&gt;&lt;a href="#cb2-5" aria-hidden="true" tabindex="-1"&gt;&lt;/a&gt;    &lt;span class="va"&gt;nixpkgs&lt;/span&gt;.&lt;span class="va"&gt;url&lt;/span&gt; &lt;span class="op"&gt;=&lt;/span&gt; &lt;span class="st"&gt;&amp;quot;github:NixOS/nixpkgs/nixpkgs-unstable&amp;quot;&lt;/span&gt;&lt;span class="op"&gt;;&lt;/span&gt;&lt;/span&gt;
&lt;span id="cb2-6"&gt;&lt;a href="#cb2-6" aria-hidden="true" tabindex="-1"&gt;&lt;/a&gt;    &lt;span class="va"&gt;darwin&lt;/span&gt;.&lt;span class="va"&gt;url&lt;/span&gt; &lt;span class="op"&gt;=&lt;/span&gt; &lt;span class="st"&gt;&amp;quot;github:lnl7/nix-darwin/master&amp;quot;&lt;/span&gt;&lt;span class="op"&gt;;&lt;/span&gt;&lt;/span&gt;
&lt;span id="cb2-7"&gt;&lt;a href="#cb2-7" aria-hidden="true" tabindex="-1"&gt;&lt;/a&gt;    &lt;span class="va"&gt;darwin&lt;/span&gt;.&lt;span class="va"&gt;url&lt;/span&gt;.&lt;span class="va"&gt;nixpkgs&lt;/span&gt;.&lt;span class="va"&gt;follows&lt;/span&gt; &lt;span class="op"&gt;=&lt;/span&gt; &lt;span class="st"&gt;&amp;quot;nixpkgs&amp;quot;&lt;/span&gt;&lt;span class="op"&gt;;&lt;/span&gt;&lt;/span&gt;
&lt;span id="cb2-8"&gt;&lt;a href="#cb2-8" aria-hidden="true" tabindex="-1"&gt;&lt;/a&gt;  &lt;span class="op"&gt;};&lt;/span&gt;&lt;/span&gt;
&lt;span id="cb2-9"&gt;&lt;a href="#cb2-9" aria-hidden="true" tabindex="-1"&gt;&lt;/a&gt;&lt;/span&gt;
&lt;span id="cb2-10"&gt;&lt;a href="#cb2-10" aria-hidden="true" tabindex="-1"&gt;&lt;/a&gt;  &lt;span class="va"&gt;outputs&lt;/span&gt; &lt;span class="op"&gt;=&lt;/span&gt; &lt;span class="op"&gt;{&lt;/span&gt; &lt;span class="va"&gt;self&lt;/span&gt;&lt;span class="op"&gt;,&lt;/span&gt; &lt;span class="va"&gt;darwin&lt;/span&gt;&lt;span class="op"&gt;,&lt;/span&gt; &lt;span class="va"&gt;nixpkgs&lt;/span&gt; &lt;span class="op"&gt;}&lt;/span&gt;: &lt;span class="op"&gt;{&lt;/span&gt;&lt;/span&gt;
&lt;span id="cb2-11"&gt;&lt;a href="#cb2-11" aria-hidden="true" tabindex="-1"&gt;&lt;/a&gt;    &lt;span class="va"&gt;darwinConfigurations&lt;/span&gt; &lt;span class="op"&gt;=&lt;/span&gt; &lt;span class="kw"&gt;rec&lt;/span&gt; &lt;span class="op"&gt;{&lt;/span&gt;&lt;/span&gt;
&lt;span id="cb2-12"&gt;&lt;a href="#cb2-12" aria-hidden="true" tabindex="-1"&gt;&lt;/a&gt;      &lt;span class="va"&gt;mba&lt;/span&gt; &lt;span class="op"&gt;=&lt;/span&gt; darwin.lib.darwinSystem &lt;span class="op"&gt;{&lt;/span&gt;&lt;/span&gt;
&lt;span id="cb2-13"&gt;&lt;a href="#cb2-13" aria-hidden="true" tabindex="-1"&gt;&lt;/a&gt;        &lt;span class="va"&gt;system&lt;/span&gt; &lt;span class="op"&gt;=&lt;/span&gt; &lt;span class="st"&gt;&amp;quot;aarch64-darwin&amp;quot;&lt;/span&gt;&lt;span class="op"&gt;;&lt;/span&gt;&lt;/span&gt;
&lt;span id="cb2-14"&gt;&lt;a href="#cb2-14" aria-hidden="true" tabindex="-1"&gt;&lt;/a&gt;        &lt;span class="va"&gt;modules&lt;/span&gt; &lt;span class="op"&gt;=&lt;/span&gt; &lt;span class="op"&gt;[&lt;/span&gt; &lt;span class="ss"&gt;./configuration.nix&lt;/span&gt; &lt;span class="op"&gt;];&lt;/span&gt;&lt;/span&gt;
&lt;span id="cb2-15"&gt;&lt;a href="#cb2-15" aria-hidden="true" tabindex="-1"&gt;&lt;/a&gt;      &lt;span class="op"&gt;};&lt;/span&gt;&lt;/span&gt;
&lt;span id="cb2-16"&gt;&lt;a href="#cb2-16" aria-hidden="true" tabindex="-1"&gt;&lt;/a&gt;    &lt;span class="op"&gt;}&lt;/span&gt;&lt;/span&gt;
&lt;span id="cb2-17"&gt;&lt;a href="#cb2-17" aria-hidden="true" tabindex="-1"&gt;&lt;/a&gt;  &lt;span class="er"&gt;}&lt;/span&gt;&lt;span class="op"&gt;;&lt;/span&gt;&lt;/span&gt;
&lt;span id="cb2-18"&gt;&lt;a href="#cb2-18" aria-hidden="true" tabindex="-1"&gt;&lt;/a&gt;&lt;span class="op"&gt;}&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;
&lt;p&gt;I put some simple system configuration in
&lt;code&gt;configuration.nix&lt;/code&gt;, built the flake with&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;$ nix build .#darwinConfigurations.mba.system&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;and was joyful to see everything working. This gave me a symlinked
directory named &lt;code&gt;result&lt;/code&gt;, containing a binary
&lt;code&gt;sw/bin/darwin-rebuild&lt;/code&gt; which I could use to bootstrap the
system from the same flake. To my dismay, I was met with an error that
said I was missing the &lt;code&gt;/run&lt;/code&gt; directory. The error message
said I needed to include the line &lt;code&gt;run\tprivate/var/run&lt;/code&gt; in
&lt;code&gt;/etc/synthetic.conf&lt;/code&gt;, which would create the &lt;a
href="https://derflounder.wordpress.com/2020/01/18/creating-root-level-directories-and-symbolic-links-on-macos-catalina/"&gt;firmlink&lt;/a&gt;
I needed. I did this without luck and ended up giving up on the whole
debacle after a long night of googling. That following morning - when I
could think sensibly again - I had an idea only a genius like me could
find: I did a &lt;em&gt;reboot&lt;/em&gt;…&lt;/p&gt;
&lt;p&gt;It worked.&lt;/p&gt;
&lt;p&gt;Okay, 1-0 to Nix, but I had no reason to give up now.
&lt;code&gt;darwin-rebuild switch --flake .&lt;/code&gt; was working, and I was off
to the races! The following days, weeks, months, I have been tinkering
and my Nix system configuration to the core, and I am nowhere near
considering myself done. If you are curious, you can explore my config
yourself on &lt;a
href="https://github.com/kmaasrud/dotfiles"&gt;GitHub&lt;/a&gt;.&lt;/p&gt;
&lt;h2 id="nix-in-projects"&gt;Nix in projects&lt;/h2&gt;
&lt;p&gt;The whole system configuration thing is very cool, but I’ve found
that the real magic of Nix comes when you start using it in your
projects.&lt;/p&gt;
&lt;p&gt;Nix serves as a very flexible build tool for your software. The
builds are done inside a siloed environment, which means you need to
declare &lt;strong&gt;all&lt;/strong&gt; the dependencies of your build. This is
&lt;em&gt;immensely&lt;/em&gt; useful! Software is built the same on all computers,
which especially makes CI a lot easier.&lt;/p&gt;
&lt;p&gt;In my project &lt;a href="https://github.com/kmaasrud/mdoc"&gt;MDoc&lt;/a&gt;,
which depends on &lt;a
href="https://tectonic-typesetting.github.io/en-US/"&gt;Tectonic&lt;/a&gt; (and
subsequently &lt;a
href="https://tectonic-typesetting.github.io/book/latest/howto/build-tectonic/index.html#third-party-dependencies"&gt;a
bunch of C/C++ libraries&lt;/a&gt;), I am able to do the build in a shell that
has all the required dependencies without actually having to manually
install said dependencies. They are listed in the following way:&lt;/p&gt;
&lt;div class="sourceCode" id="cb4"&gt;&lt;pre
class="sourceCode nix"&gt;&lt;code class="sourceCode nix"&gt;&lt;span id="cb4-1"&gt;&lt;a href="#cb4-1" aria-hidden="true" tabindex="-1"&gt;&lt;/a&gt;buildInputs = &lt;span class="kw"&gt;with&lt;/span&gt; pkgs&lt;span class="op"&gt;;&lt;/span&gt; &lt;span class="op"&gt;[&lt;/span&gt;&lt;/span&gt;
&lt;span id="cb4-2"&gt;&lt;a href="#cb4-2" aria-hidden="true" tabindex="-1"&gt;&lt;/a&gt;  fontconfig&lt;/span&gt;
&lt;span id="cb4-3"&gt;&lt;a href="#cb4-3" aria-hidden="true" tabindex="-1"&gt;&lt;/a&gt;  graphite2&lt;/span&gt;
&lt;span id="cb4-4"&gt;&lt;a href="#cb4-4" aria-hidden="true" tabindex="-1"&gt;&lt;/a&gt;  harfbuzz&lt;/span&gt;
&lt;span id="cb4-5"&gt;&lt;a href="#cb4-5" aria-hidden="true" tabindex="-1"&gt;&lt;/a&gt;  icu&lt;/span&gt;
&lt;span id="cb4-6"&gt;&lt;a href="#cb4-6" aria-hidden="true" tabindex="-1"&gt;&lt;/a&gt;  libpng&lt;/span&gt;
&lt;span id="cb4-7"&gt;&lt;a href="#cb4-7" aria-hidden="true" tabindex="-1"&gt;&lt;/a&gt;  perl&lt;/span&gt;
&lt;span id="cb4-8"&gt;&lt;a href="#cb4-8" aria-hidden="true" tabindex="-1"&gt;&lt;/a&gt;  openssl&lt;/span&gt;
&lt;span id="cb4-9"&gt;&lt;a href="#cb4-9" aria-hidden="true" tabindex="-1"&gt;&lt;/a&gt;  zlib&lt;/span&gt;
&lt;span id="cb4-10"&gt;&lt;a href="#cb4-10" aria-hidden="true" tabindex="-1"&gt;&lt;/a&gt;&lt;span class="op"&gt;]&lt;/span&gt; &lt;span class="op"&gt;++&lt;/span&gt; lib.optionals stdenv.isDarwin &lt;span class="op"&gt;[&lt;/span&gt;&lt;/span&gt;
&lt;span id="cb4-11"&gt;&lt;a href="#cb4-11" aria-hidden="true" tabindex="-1"&gt;&lt;/a&gt;  ApplicationServices&lt;/span&gt;
&lt;span id="cb4-12"&gt;&lt;a href="#cb4-12" aria-hidden="true" tabindex="-1"&gt;&lt;/a&gt;  Cocoa&lt;/span&gt;
&lt;span id="cb4-13"&gt;&lt;a href="#cb4-13" aria-hidden="true" tabindex="-1"&gt;&lt;/a&gt;&lt;span class="op"&gt;]&lt;/span&gt;;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;
&lt;p&gt;Nix uses this list and makes sure these are linkable when building -
&lt;strong&gt;so smooth!&lt;/strong&gt; As you can see, &lt;q&gt;software is built the
same on all computers&lt;/q&gt; wasn’t an entirely true statement after all,
since I need some specific dependencies for Darwin systems. However,
when they can be conditionally defined this way, who am I to judge.&lt;/p&gt;
&lt;h2 id="conclusion"&gt;Conclusion&lt;/h2&gt;
&lt;p&gt;Allright, that is a misleading header; My Nix journey has only just
begun. What I can say, is that Nix has changed the way I look at
software. It would be very hard to go back to the old ways of a mutable
system and non-reproducible development environments. I am convinced
that Nix is the future - in some way or the other. It just needs a &lt;a
href="https://nixos.wiki/wiki/Nix_command"&gt;facelift&lt;/a&gt;, because there
is no way I am accepting &lt;code&gt;nix-*&lt;/code&gt; type commands.&lt;/p&gt;</content>
</entry>
<entry>
<title>A love letter to make</title>
<link href="https://kmaasrud.com/blog/make.html" />
<id>D2C5C773-A1F0-4CAE-9C9B-0AF451644462</id>
<published>2023-04-19T13:00:00+02:00</published>
<content type="html">&lt;h1 id="a-love-letter-to-make"&gt;A love letter to make&lt;/h1&gt;
&lt;!-- atom-id: D2C5C773-A1F0-4CAE-9C9B-0AF451644462 --&gt;
&lt;p&gt;&lt;time datetime="2023-04-19T13:00:00+02:00"&gt;&lt;/p&gt;
&lt;p&gt;Dear make,&lt;/p&gt;
&lt;p&gt;As I sit here thinking about you, I can’t help but feel a rush of
excitement in my heart. I know that to some, you may seem like a dry and
unromantic tool, but to me, you are the epitome of true love. Your
simple syntax and powerful capabilities have stolen my heart and made my
programming process so much smoother. Who knows how many keystrokes you
have saved me since we first met? I used to spend hours typing out long
and complex build commands, but now, with you by my side, those days are
long gone. I shudder to think about how much time and energy I would
have wasted without you.&lt;/p&gt;
&lt;p&gt;Your knack for automating commands and streamlining my build, is like
a loving partner who knows exactly what I need before I even ask. It’s
as if you have a sixth sense for my needs and desires. You’re like a
chameleon that can adapt to any language or environment. That kind of
versatility is seriously impressive, and it makes me feel like you’re
always up for a new adventure.&lt;/p&gt;
&lt;p&gt;And what really sets you apart, is your reliability. You never let me
down or give me any trouble. You always do exactly what I tell you to
do, without any fuss or drama, and you’re always there wherever I go.
That kind of dependability is what every programmer dreams of in a
partner.&lt;/p&gt;
&lt;p&gt;But, let’s be real here, make, you’re not perfect. I have to admit
that sometimes you can be a little old-fashioned and quirky, with your
insistence on using tabs instead of spaces, and your archaic syntax that
can take some getting used to. And don’t even get me started on those
&lt;code&gt;.PHONY&lt;/code&gt; targets. Despite your quirks and flaws though, I
still love you. Because when it comes down to it, your benefits far
outweigh your drawbacks. You make my programming process so much more
efficient and streamlined, and you save me countless hours of tedious
work. Plus, you’re always there for me when I need you, and you never
let me down.&lt;/p&gt;
&lt;p&gt;So here’s to you, make - quirks and all. You may not be perfect, but
you’re pretty darn close. Thank you for being the tool that I never knew
I needed, and for making my life as a programmer so much easier and more
enjoyable.&lt;/p&gt;
&lt;p&gt;With love and a pinch of frustration,&lt;/p&gt;
&lt;p&gt;A Hopelessly Devoted Programmer&lt;/p&gt;</content>
</entry>
<entry>
<title>You should track your finances in TOML</title>
<link href="https://kmaasrud.com/blog/track-finances-in-toml.html" />
<id>4738FCCA-E1AE-42AE-9F64-D519DBC1CCE0</id>
<published>2023-11-13T13:00:00+01:00</published>
<content type="html">&lt;h1 id="you-should-track-your-finances-in-toml"&gt;You should track your
finances in TOML&lt;/h1&gt;
&lt;!-- atom-id: 4738FCCA-E1AE-42AE-9F64-D519DBC1CCE0 --&gt;
&lt;p&gt;&lt;time datetime="2023-11-13T13:00:00+01:00"&gt;&lt;/p&gt;
&lt;p&gt;No, really! Let me explain.&lt;/p&gt;
&lt;p&gt;I am quite particular with my finances. Having a proper accounting
system helps me keep control of my spendings and what I am saving. Being
a pretty nerdy guy, I quickly gravitated towards the plain text
accounting movement, with which I ensure that I can control my data,
switch systems if I want and do custom scripting around my ledger. I
won’t delve into the details here, but the &lt;a
href="https://plaintextaccounting.org"&gt;plaintextaccounting.org&lt;/a&gt;
website is a great source of information, and I recommend reading
through the &lt;a
href="https://github.com/plaintextaccounting/plaintextaccounting/wiki/Newcomer-FAQ"&gt;Newcomer
FAQ&lt;/a&gt; if you want an introduction to the concept.&lt;/p&gt;
&lt;p&gt;The most popular tools for plain text accounting today are &lt;a
href="https://github.com/beancount/"&gt;Beancount&lt;/a&gt;, &lt;a
href="https://ledger-cli.org/"&gt;Ledger&lt;/a&gt; and &lt;a
href="https://hledger.org/"&gt;hledger&lt;/a&gt;. They are all fantastic tools,
using similarly inspired DSLs that are pretty easy to learn. However,
their syntax deviate in subtle ways. Here is an example entry written in
Ledger first and then Beancount:&lt;/p&gt;
&lt;pre class="ledger"&gt;&lt;code&gt;2023/11/06 My local grocery store | Grocery shopping
  Assets:ExampleBank:Checkings  $-20
  Expenses:Food:Groceries        $20&lt;/code&gt;&lt;/pre&gt;
&lt;pre class="beancount"&gt;&lt;code&gt;2023-11-06 * &amp;quot;My local grocery store&amp;quot; &amp;quot;Grocery shopping&amp;quot;
  Assets:ExampleBank:Checkings  -20 USD
  Expenses:Food:Groceries        20 USD&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;They are not compatible with eachother, and converting between the
two is non-trivial. There are countless examples of features
(transaction flags, prices, capitalization conventions, metadata, etc.)
that are non-interoperable like this.&lt;/p&gt;
&lt;p&gt;Now say you want to write some scripts for processing your journal.
This means you’ll have to scour the internet for a parser supported by
your language of choice, or write one on your own. Let me tell you, they
are not abundant, and good luck finding a non-official library that is
regularly updated. You might get forced to use a language you would not
choose yourself, just to get some simple processing done.&lt;/p&gt;
&lt;p&gt;Many of the reasons these tools have opted for a DSL, is to keep the
amount of necessary typing to a minimum—an admirable goal which they
achieve quite well in my opinion. However, most users use automated
scripts, data-entry tools or at the least some editor completion to
insert their data. Why, then, can’t we just use a standard plain text
data serialization language?&lt;/p&gt;
&lt;h2
id="advantages-of-using-a-proper-data-serialization-format"&gt;Advantages
of using a proper data serialization format&lt;/h2&gt;
&lt;p&gt;The advantages are obvious, given my gripes listed above; almost all
of them are solved:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;Interoperability: It is trivial to convert from one data format to
another. Just look at the &lt;a href="https://serde.rs/"&gt;serde&lt;/a&gt;
framework for Rust, or the countless number of &lt;a
href="https://transform.tools/"&gt;conversion tools&lt;/a&gt; available
online.&lt;/li&gt;
&lt;li&gt;Language support: You can expect to find a high-quality maintaned
library for parsing any of the big data formats in most languages. These
often also deserialize into native data-structures, which should be
easier to handle when scripting.&lt;/li&gt;
&lt;li&gt;Standard: This hooks a bit into interoperability, but the key here
is that you can expect your data to be easily used by or ported to any
tool or service you want to use in the future. Not only that, but having
a simple map to e.g. JSON makes the path to a web-accessible API quite
clear.&lt;/li&gt;
&lt;/ul&gt;
&lt;h2 id="why-toml"&gt;Why TOML?&lt;/h2&gt;
&lt;p&gt;With these ideas in mind, why am I saying that TOML specifically is
the solution? Well, &lt;a href="https://toml.io/en/"&gt;the homepage&lt;/a&gt; says
it all:&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;A config file format for humans.&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;The whole ethos around plain text accounting is that it can be read
in it’s raw form and edited by hand if so desired. These goals are at
the core of TOMLs design. Here is the equivalent transaction I showed
above, written in my proposed TOML ledger specification:&lt;/p&gt;
&lt;div class="sourceCode" id="cb3"&gt;&lt;pre
class="sourceCode toml"&gt;&lt;code class="sourceCode toml"&gt;&lt;span id="cb3-1"&gt;&lt;a href="#cb3-1" aria-hidden="true" tabindex="-1"&gt;&lt;/a&gt;&lt;span class="kw"&gt;[[transaction]]&lt;/span&gt;&lt;/span&gt;
&lt;span id="cb3-2"&gt;&lt;a href="#cb3-2" aria-hidden="true" tabindex="-1"&gt;&lt;/a&gt;&lt;span class="dt"&gt;date&lt;/span&gt; &lt;span class="op"&gt;=&lt;/span&gt; &lt;span class="bn"&gt;2023-11-06&lt;/span&gt;&lt;/span&gt;
&lt;span id="cb3-3"&gt;&lt;a href="#cb3-3" aria-hidden="true" tabindex="-1"&gt;&lt;/a&gt;&lt;span class="dt"&gt;description&lt;/span&gt; &lt;span class="op"&gt;=&lt;/span&gt; &lt;span class="st"&gt;&amp;quot;Grocery shopping&amp;quot;&lt;/span&gt;&lt;/span&gt;
&lt;span id="cb3-4"&gt;&lt;a href="#cb3-4" aria-hidden="true" tabindex="-1"&gt;&lt;/a&gt;&lt;span class="dt"&gt;payee&lt;/span&gt; &lt;span class="op"&gt;=&lt;/span&gt; &lt;span class="st"&gt;&amp;quot;My local grocery store&amp;quot;&lt;/span&gt;&lt;/span&gt;
&lt;span id="cb3-5"&gt;&lt;a href="#cb3-5" aria-hidden="true" tabindex="-1"&gt;&lt;/a&gt;&lt;span class="dt"&gt;postings&lt;/span&gt; &lt;span class="op"&gt;=&lt;/span&gt; &lt;span class="op"&gt;[&lt;/span&gt;&lt;/span&gt;
&lt;span id="cb3-6"&gt;&lt;a href="#cb3-6" aria-hidden="true" tabindex="-1"&gt;&lt;/a&gt;  &lt;span class="op"&gt;{ &lt;/span&gt;&lt;span class="dt"&gt;account&lt;/span&gt;&lt;span class="op"&gt; =&lt;/span&gt; &lt;span class="st"&gt;&amp;quot;Assets:ExampleBank:Checkings&amp;quot;&lt;/span&gt;&lt;span class="op"&gt;, &lt;/span&gt;&lt;span class="dt"&gt;amount&lt;/span&gt;&lt;span class="op"&gt; =&lt;/span&gt; &lt;span class="dv"&gt;-20&lt;/span&gt;&lt;span class="op"&gt;, &lt;/span&gt;&lt;span class="dt"&gt;currency&lt;/span&gt;&lt;span class="op"&gt; =&lt;/span&gt; &lt;span class="st"&gt;&amp;quot;USD&amp;quot;&lt;/span&gt;&lt;span class="op"&gt; }&lt;/span&gt;&lt;/span&gt;
&lt;span id="cb3-7"&gt;&lt;a href="#cb3-7" aria-hidden="true" tabindex="-1"&gt;&lt;/a&gt;  &lt;span class="op"&gt;{ &lt;/span&gt;&lt;span class="dt"&gt;account&lt;/span&gt;&lt;span class="op"&gt; =&lt;/span&gt; &lt;span class="st"&gt;&amp;quot;Expenses:Food:Groceries&amp;quot;&lt;/span&gt;&lt;span class="op"&gt;, &lt;/span&gt;&lt;span class="dt"&gt;amount&lt;/span&gt;&lt;span class="op"&gt; =&lt;/span&gt; &lt;span class="dv"&gt;20&lt;/span&gt;&lt;span class="op"&gt;, &lt;/span&gt;&lt;span class="dt"&gt;currency&lt;/span&gt;&lt;span class="op"&gt; =&lt;/span&gt; &lt;span class="st"&gt;&amp;quot;USD&amp;quot;&lt;/span&gt;&lt;span class="op"&gt; }&lt;/span&gt;&lt;/span&gt;
&lt;span id="cb3-8"&gt;&lt;a href="#cb3-8" aria-hidden="true" tabindex="-1"&gt;&lt;/a&gt;&lt;span class="op"&gt;]&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;
&lt;p&gt;Definitely more verbose, yes, but still reasonably short and totally
legible (and perhaps even easier to understand without knowing the
syntax beforehand.) Note also that TOML has a &lt;a
href="https://toml.io/en/v1.0.0#offset-date-time"&gt;datetime&lt;/a&gt; type,
which is perfectly suited for this use case.&lt;/p&gt;
&lt;p&gt;Now this example leaves a lot to be desired. Evidently, writing out
the currency in this way for each posting is not ideal, but could be
solved by allowing the user to set a default currency. Accounts also
borrow the colon-separated syntax from ledger, but I think this is also
something that could be improved upon. There are also a lot of crucial
details I have glossed over here—for example currency prices, posting
metadata, balance assertions and more—but I would say these are all
pretty low-hanging fruit.&lt;/p&gt;
&lt;p&gt;If you like the concept and have thoughts or ideas that you would
like to share, please send an email to my &lt;a
href="https://lists.sr.ht/~kmaasrud/inbox"&gt;public inbox&lt;/a&gt;, and I’ll be
happy to discuss! I’m doing some testing in a CLI called &lt;a
href="https://git.sr.ht/~kmaasrud/mynt"&gt;mynt&lt;/a&gt;, which initially is for
personal use, but feel free to have a look at it for reference,
contribute a patch if you see an improvement that can be made or even
try it out yourself.&lt;/p&gt;</content>
</entry>
</feed>
