Colophon
I think every personal blog that is somewhat custom built should have documentation written on its design philosophy and implementation. I see this as an extension of the free-software ideal; you should not only be allowed but also able to extent and modify another program to your needs. It is also useful as a way to discover other peoples’ inspirations and influences, as well as finding unique sources of inspiration for your own website.
Philosophy
This site exists in the overlap between a landing page, a traditional blog, a hypomnemata, and what is called a “digital garden”. I originally used the domain merely to host services and small programs over the open internet, and to have a place to point to and say “this is where you can find me online”. I started writing things because I found that certain topics kept coming up in discussion with the people around me, and I wanted to summarise my thoughts on them. A website was the perfect place for that.
I write many pieces concurrently, and publish them either when I feel they are finished or when my motivation has drained and I feel like I need to “just get it out there”. In some cases I also get a large boost of energy and write a post in even just a day or two, although I often try and stop myself from publishing immediately in order to let my thoughts simmer for a bit.
I then expand on topics if I come up with new perspectives or need to respond do a new development. In this way posts exist in a space between being mere ephemeral blog posts that were written at a given time and a continually rewritten and updated page. I try to avoid changing the main theses of a post after initial publication — even if I have come to disagree with myself over time — and so they still reflect the version of myself when they were first written.
The website is written in gnu Emacs and in org mode, because I find the markup to be intuitive, natural, and easy.1
Fonts
The primary font used is Sabon. Code uses the Greybeard font. The headers are all typeset in the beautiful DaVinci by the nyc creative bureau Virgile Flores. For 中文 (and other cjk languages) I use Kai Ti since the default Noto font installed on many devices clashed quite a lot with the thin serif fonts I otherwise use.
I jump quite often between fonts, especially for the serif font for body text. In this way having a website is quite useful; it allows me to experiment widely with design in an actual production environment.
Inspirations
I generally took a lot of inspiration from others using the same tooling as me, as well as from Gwern, Maggie Appleton and various other blogs and homepages I have read over the years. The colour scheme is a slightly adapted version of Protesilaos Stavrou’s modus themes (for the light theme) and the paradise theme (for the dark). I find them visually pleasing, and the Modus theme is very accessible to those with impaired vision. I have used them in Emacs for a long time, and so they fit nicely on my desktop when looking at my site.
The one-rule css implementation of the drop caps is taken from Michał
Sapka, while the font files were downloaded from Gwern using a quick
curl-script I had an llm write.2 The font is “Baroque Initials”,
more information here. The indented paragraphs were inspired by Miró
Allard.
The little arrows following external links (of which there have been quite a lot in this section) are from Alex Wennerberg, whose simple design I find admirable. Using email as a comment system was taken from Marcel Wichmann.
Static website generation
The blog is generated using Bastian Bechtold’s org-static-blog. I have
an elisp file that runs the necessary configuration code and acts like
a declarative configuration for the deployment of the static site. It
looks like this:
(setq org-static-blog-publish-title "Joar von Arndt") (setq org-static-blog-publish-url "https://joarvarndt.se/") (setq org-static-blog-publish-directory "~/Documents/blog") (setq org-static-blog-posts-directory "~/Documents/blog/posts/") (setq org-static-blog-drafts-directory "~/Documents/blog/drafts/") (setq org-static-blog-enable-tags t) (setq org-static-blog-enable-og-tags t) (setq org-static-blog-image "https://joarvarndt.se/vonArndtCrestWhite.png") (setq org-static-blog-use-preview t) (setq org-static-blog-no-comments-tag "nocomment") (setq org-static-blog-enable-tag-rss t) (setq org-static-blog-index-length 6) (setq org-static-blog-preview-link-p t) (setq org-static-blog-preview-ellipsis "<span class=\"ellipsis\">Continue Reading</span>")
These are all simple configuration values that are unique to
org-static-blog, and where most of the values are themselves unique to
this blog specifically.
(setq org-export-with-toc t) (setq org-export-with-broken-links t) (setq org-export-with-smart-quotes t) (setq org-export-with-section-numbers nil) (setq org-image-actual-width nil) ;; For syntax highlighting of code (Like this). (setq org-src-fontify-natively t) (setq org-html-htmlize-output-type 'css) (setq org-src-preserve-indentation t)
org-static-blog uses org-mode’s built-in html export functionality for
much of the “dirty work”, and these variables are used for all
org-mode exports. The latter part allows Emacs to transform its own
syntax highlighting into css classes that I can then highlight with
css variables depending on dark/light theme. This is all done at the
rendering stage, meaning things like highlight.js are
unnecessary. Without setting these variables Emacs will hard-code the
current Emacs theme colours into the html.
(setq org-static-blog-display-git-date t) (setq org-static-blog-title-link nil) (setq org-static-blog-hidden-directory "~/Documents/blog/hidden/")
These are variables used in my own custom fork of
org-static-blog. This toggles the display of the “last edited” date at
the top of the post page, derived from when the .org-file was last
changed. All the original org-mode files are uploaded to the website,
and can be accessed by navigating to
“https://joarvarndt.se/posts/postname.org”.
By default org-static-blog renders the post title of each page as a
link pointing to itself. I found this quite distracting as it means
displaying responsive changes whenever the title is hovered over. This
variable thus disables that functionality.
The hidden directory acts as a sort of middle-ground between drafts
and posts; I have the drafts directory marked in the .gitignore file
and can therefore not use git to track changes made to files in that
directory, but I do not want pages like subscribe, about, or the link
page to show up in the archive page as a proper “post”. For this I
use the hidden directory.
(setq org-static-blog-preview-start "<p class=\"dcap\">")
I have written a function that applies the dcap class to the first
normal paragraph in a post, but adding such a class means that the
default “<p>” value will match the second paragraph instead of the
first. This change thus needs to be made.
(setq org-static-blog-page-header "<meta name=\"Joar von Arndt\" content=\"Joar von Arndt\"> <meta name=\"referrer\" content=\"no-referrer\"> <meta name=\"viewport\" content=\"initial-scale=1,width=device-width,minimum-scale=1\"> <link href=\"static/style.css\" rel=\"stylesheet\" type=\"text/css\" /> <link rel=\"icon\" href=\"static/favicon.ico\"> <link rel=\"me\" href=\"https://github.com/JanJoar\" /> <link rel=\"me\" href=\"https://gravatar.com/speedilyrunaway0201f99245\" /> <link rel=\"webmention\" href=\"https://webmention.io/joarvarndt.se/webmention\" /> <script src=\"static/sidenotes.js\" defer></script> <script src=\"static/comments.js\" defer></script> <link rel=\"stylesheet\" href=\"https://cdn.jsdelivr.net/npm/katex@0.16.33/dist/katex.min.css\" integrity=\"sha384-fgys3vc1089n2j3rVcEbxdhlndlq9b2y1hvpq720q1NvxCduQqt4joGc4u2qcnzE\" crossorigin=\"anonymous\"> <script defer src=\"https://cdn.jsdelivr.net/npm/katex@0.16.33/dist/katex.min.js\" integrity=\"sha384-yphnapyrxgs8bnna7q4ommqra8wqpejoovslzFgwgs8oxjbvadbyvx4QpfiFurGr\" crossorigin=\"anonymous\"></script> <script defer src=\"https://cdn.jsdelivr.net/npm/katex@0.16.33/dist/contrib/auto-render.min.js\" integrity=\"sha384-jkxhijf8pkpydfptukzoUymrqjamqkj4b4xyOca62ebJhcimygiDdq/9twuuwyzh\" crossorigin=\"anonymous\" onload=\"renderMathInElement(document.body);\"></script>") (setq org-static-blog-post-comments "<div class=\"comment-by-email\"> <form class=\"comment-by-email-form\"> <textarea id=\"comment-by-email-textarea\" placeholder=\"Write a comment\"></textarea> <button type=\"submit\">Send</button> </form> </div>")
This is quite self-explanatory, it formats the header for each page so that the correct css, javascript, favicons et cetera are loaded. While the website does make use of javascript I have as an utmost priority that it should be accessible and readable without it. There are really only three elements that require javascript:
- Comments, who quite simply creates a
mailto:-link that is filled in with the appropriate information (including recipient, subject header, and content inserted on the website). All of this can of course be done manually by emailing me the old-fashioned way. - Client-side \(\LaTeX\) rendering using \(\KaTeX\). This should be possible to render server-side and therefore remove, but I have yet to build a way to do so using my ssg. A project for the future.
- Sidenotes. I cover this further down.
(setq org-static-blog-page-preamble "<div class=\"header\" role=\"banner\"> <a href=\"https://joarvarndt.se/\" class=\"home-link\"><img id=\"themeImageHeader\" src=\"vonArndtCrestBlack.png\" class=\"header-img\" alt=\"Crest\" width=\"50\"></a></div>") (setq org-static-blog-page-postamble "<div id=\"postamble\" class=\"status\"><div class=\"footer\" role=\"contentinfo\"> <div class=\"footer-upper\"> <footer-img><img loading=\"lazy\" class=\"footer-img\" id=\"themeImageFooter\" src=\"Ship-5.svg\" alt=\"Set sail on the sea of knowledge\"></footer-img> <copyright>© 2026 <span class=\"small-caps\">ad</span> / 234 <span class=\"small-caps\">ar</span> Joar von Arndt</copyright> </div> <div class=\"footer-lower\"> <nav> <ul> <li><a href=\"https://joarvarndt.se/about.html\">About</a></li> <li><a href=\"https://joarvarndt.se/archive.html\">Archive</a></li> <li><a href=\"https://joarvarndt.se/links.html\">Your next stop</a></li> <li><a href=\"https://joarvarndt.se/website.html\">Colophon</a></li> </ul> </nav> <nav> <ul> <li><a href=\"https://joarvarndt.se/now.html\">Now</a></li> <li><a href=\"https://joarvarndt.se/subscribe.html\">Subscribe</a></li> <li><a href=\"https://joarvarndt.se/contact.html\">Contact</a></li> <li><a href=\"https://webmention.io/joarvarndt.se/webmention\">Webmentions</a></li> </ul> </nav> <script src=\"static/theme.js\" defer></script> </div></div></div>")
The header and footer of each page. The header is very plain — it only includes the heraldic achievement that acts as a “home” link. The table of contents that appears on some pages (including this one) is a part of the post itself and is generated by org-mode’s export functionality. It simply moves to the top of the page using css and is not part of the header.
The footer has comparatively quite a lot going on. Its multi-column
design was inspired principally by Jonas Hietala, from whom I was also
partly inspired by the choice of using git to track post changes
(although the idea of tracking changes to a post came from Appleton’s
“planted X time ago”). I also chose to prominently display webmentions
as a technology since they are used by an unfortunately few number of
people, and I myself often forget to check if a site does or not when
linking to them.
(setq org-static-blog-index-front-matter "<site-intro><p> This is the site of Joar Alexander Pablo von Arndt. I write about a variety of topics, from coverage of <a href=\"https://joarvarndt.se/eww.html\"><span class=\"small-caps\">gnu</span> Emacs web browsing</a>, to <a href=\"https://joarvarndt.se/business_cards.html\">graphic design</a> and <a href=\"https://joarvarndt.se/european-tech.html\">critiques of industrial policy</a>. You can <a href=\"https://joarvarndt.se/subscribe.html\"> subscribe here</a>. If you enjoy anything written here (or disagree with me) feel free to <a href=\"https://joarvarndt.se/contact.html\">tell me about it</a>. </p> <ul class=\"org-ul\"> <li><a href=\"https://joarvarndt.se/archive.html\" class=\"archive-link\">All Posts</a></li> </ul> <p> My own personal favorites are the following: </p> <ul class=\"org-ul\"> <li><a href=\"https://joarvarndt.se/end-of-history.html\"><i>The End of History.</i></a></li> <li><a href=\"https://joarvarndt.se/email.html\"><i>Email as a Revolutionary Medium</i></a></li> <li><a href=\"https://joarvarndt.se/deepseek.html\"><i>Deepseek is not a Chinese Openai</i></a></li> </ul> <nav id=\"table-of-contents\" role=\"doc-toc\"> <h2><i>Itinerarium</i></h2> <div id=\"text-table-of-contents\" role=\"doc-toc\"> <ul> <li><a href=\"https://joarvarndt.se/archive.html\">All Posts</a></li> <li><a href=\"https://joarvarndt.se/about.html\">About</a></li> <li><a href=\"https://joarvarndt.se/now.html\">Now</a></li> <li><a href=\"https://joarvarndt.se/links.html\">Your next stop</a></li> <li><a href=\"https://joarvarndt.se/subscribe.html\">Subscribe</a></li> <li><a href=\"https://joarvarndt.se/website.html\">Colophon</a></li> </ul> </div> </nav> </site-intro>")
This is the hand-crafted html for the index page, that itself has no
corresponding org-mode file. It is generated on each rebuild with this
introduction and a series of posts governed by
org-static-blog-index-length. The itinerarium that acts as a sort of
site-menu is formatted like a custom table of contents for the index
page.
(setq org-static-blog-archive-front-matter "<picture> <source srcset=\"https://joarvarndt.se/bird-butterfly.webp\" type=\"image/webp\"> <source srcset=\"https://joarvarndt.se/bird-butterfly.png\" type=\"image/png\"> <img src=\"https://joarvarndt.se/bird-butterfly.png\" alt=\"A small bird resting amongst the branches, gazing at a flying butterfly.\" class=\"pictograph\"> </picture> <hr>")
Finally another custom-fork-feature; archive front matter! This can be used to display anything on the page listing all of the posts, but here I use it to insert a beautiful illustration that I got from Tom Chalky.3
The above lisp code is not just a series of example code snippets, it is directly the code that is used to build the website! The text you are reading right now is written as an org-mode document, and the code blocks above are exported into a corresponding Emacs lisp file and run as part of the build process. Because it became annoying to manually evaluate the above code block every time I wanted to export I wrote a quick build script, whose contents can easily be embedded4 into this post like this:
#+include: "~/Documents/blog/build" src emacs-lisp
#!/usr/bin/emacs -x ;;; Code: (package-initialize) (message "Initialized package.el") (require 'org) (require 'cal-french) (require 'htmlize) (message "Loaded libraries") (org-babel-tangle-file "~/Documents/blog/posts/website.org") (load-file "~/Documents/blog/posts/website.el") (load-file "~/programming/org-static-blog/utils.el") (load-file "~/programming/org-static-blog/org-static-blog.el") (org-static-blog-publish) ;;; build ends here
This makes use of the very underused scripting capabilities of Emacs
(#!/usr/bin/emacs -x), allowing you to use the power of lisp combined
with Emacs’ extensive libraries. It also loads my personal fork of
org-static-blog that makes some small opinionated changes (like the
git-edit and french republican dates). The french republican dates are
one of the clearest examples of the usefulness of Emacs as a standard
library — Emacs ships with a library to translate to and from the
french republican calendar.
I am a big fan of org-static-blog due to its simplicity5,
extensibility (it is just a simple html static site generator) and
obvious org-mode support. That I wrote my own fork is testament to the
great work done by Bechtold. It is incredibly featureful, simple to
read and to modify. If you ever wonder how a given piece of html is
generated you can quickly C-s around in the source code (all of which
is in a single file) between function definitions and implementations.
It is also quite fast — the majority of the build script is usually
just start-up time from Emacs loading libraries — but not fast enough
to offer anything like real-time previews. Unless
org-static-blog-publish is called with a prefix argument6 it will
only re-render those files that have actually changed and thus doesn’t
take very long to re-render the site. Only when tinkering with
site-wide html changes (such as for the footer) do I need to re-render
everything, and even then it just takes a few seconds.
I used to use Hugo, a widely praised static website generator, but it only has nominal org-mode support and requires a special non-org-mode yaml frontmatter that I really dislike. When writing a post, I prefer to leave formatting mostly out of it, and with my main focus on text. Tinkering with the website’s appearance is great fun of course, but I like to keep the two as separate activities. Keeping them separate means that if I improve the appearance of a new post all previous posts also benefit. Writing any sort of special syntax also brings me out of the experience, and that is partly why I wrote my own javascript to create sidenotes out of basic pandoc (and therefore org-mode) style footnotes instead of using tufte-css.
Sidenotes
The sidenotes are heavily inspired by Gwern and Tufte css, but the
Gwern sidenotes.js is not portable and seems way too complex for my
needs (it is 1150 lines long!!). In comparison, here is the the script
that creates sidenotes. It is merely 70 lines long, including
whitespace and comments. It also allows for easy reading on mobile,
vertical monitors, and js-free browsers, since if the dimensions are
not adequate for displaying sidenotes or the javascript does not run
the content does not disappear and is presented as simple
footnotes.
Tufte css’ sidenotes also appear strange when viewed through rss-readers or text-based browsers like eww. Moving the text only when javascript is available preserves the structure of the raw html documents when viewed without javascript. That Tufte css does not make use of javascript in the first place is quite admirable, but it is a shame that doing so breaks many of the applications where javascript is not available. This is then a deliberate choice of using javascript to improve accessibility. I could have used pure css/html sidenotes, but doing so would likely break many non-traditional browsers.
I have however taken the liberty of inspiration from Tufte css in the css of sidenotes and margin notes. Margin notes are written in the form of an org-mode export block, and so have to be located at the beginning or end of paragraph.
This is how I write a margin note!
#+begin_marginnote This is how I write a margin note! #+end_marginnote
I can do this since org treats “special blocks” as a <div> with a
synonymously named marginnote class when exported to html. This can
then be styled however you want using css. Margin notes are usually
not as critical in my mind as foot/sidenotes, since they serve merely
as a general guide to the text rather than an explicit pointed
“reference”7. That’s why margin notes are not displayed on mobile
devices. This is an opinionated change from Tufte css’ approach, where
margin notes are instead treated as unnumbered sidenotes when there is
no margin to display them in. Margin notes are best considered as a
sort of “scribble” in the margin, and attaching them to any specific
point in the text makes them merely inferior sidenotes.
Small caps
It is typographical best practice to typeset segments of capitalised text in “small caps” that are the same height as lowercase text. (like this). If you have a keen eye you will no doubt have noticed this this in the many acronyms used throughout this colophon.8 This is provided by a css option:
span.small-caps { font-variant: small-caps; }
But how do we apply this element to segments of text? Org-mode’s syntax does not provide for any shorthand for this sort of text, and really we would prefer to to apply such a formatting to our beautiful plain text. There is also the added downside of in that case having to make changes to the large number of acronyms used in all previous posts.
But we can instead do this using the Emacs build process. This Emacs
lisp function wraps all series of capitalised text (a-z) greater than
2 in the small-caps span element:
(defun org-static-blog--wrap-acronyms (html) "Wrap sequences of multiple uppercase letters in <span class=\"small-caps\"> tags. Does not wrap acronyms inside href attributes." (let ((result html) (href-values '()) (href-counter 0) (case-fold-search nil)) (setq result (replace-regexp-in-string "href=\"\\([^\"]*\\)\"" (lambda (match) (let ((href-content (match-string 1 match)) (placeholder (format "href=\"__xhref_protected_%d__\"" href-counter))) (push href-content href-values) (setq href-counter (1+ href-counter)) placeholder)) result)) (setq href-values (nreverse href-values)) ;; Wrap all sequences of uppercase letters (optionally with &/&, ., or - between them) (setq result (replace-regexp-in-string "\\([a-z]\\(?:\\(?:&\\(?:amp;\\)?\\|[.-]\\)?[a-z]\\)+\\)" (lambda (match) (concat "<span class=\"small-caps\">" (downcase (match-string 1 match)) "</span>")) result t)) ;; Restore href values (let ((counter 0)) (dolist (href-val href-values) (setq result (replace-regexp-in-string (format "__xhref_protected_%d__" counter) (lambda (_match) href-val) result t)) (setq counter (1+ counter)))) result))
It is quite long and complicated merely because there are a few major
edge cases that need to be addressed, such as acronyms used within the
href part of links (that would otherwise break) and acronyms like r&d,
e2ee, f-150, or ww1 that do not consist of simple all-caps letters.
This means that I can write in all-caps in the plain-text document and have it rendered using small caps automatically. It is also backwards compatible with everything that I have written previously, it merely requires a re-rendering of the html.
Hosting
The website used to be hosted on my own server at home, on an Intel nuc machine, running at first nginx and then Caddy.9 After I experienced a 25 day long downtime in June 2025 when I was traveling in China I however decided to not host the public-facing blog from home. These types of downtimes are not acceptable to me and my residential internet connection is simply too unreliable to maintain a high uptime anyways. For that reason I have moved the blog to Netlify but still retain the server for hosting personal services and experiments.
Netlify requires a git repository at one of a small list of git forges, so I have a private repository of the blog on Github that is automatically pushed to from the main repository on Codeberg.
I use loopia as my domain registrar, primarily due to having been recommended them from someone I trust, however I have made no significant research into alternatives myself.
I use magit to interact with git and push changes to the blog to a
remote repository that then causes netlify to update. ❦
Footnotes:
Why would any markup not use “//” to italicise text? Markdown’s
use of just asterisks is admirable, but unintuitive for reading in
plaintext (Does two asterisks mean italic or bold?). Org mode’s choice
of using __ to underline text is similarly elegant.
set -euo pipefail base="https://gwern.net" destdir="~/Documents/blog/static/fonts/" mkdir -p "$destdir" for L in {A..Z}; do file="Yinit-${L}.ttf" url="${base}/static/font/dropcap/yinit/${file}" echo "Downloading $file..." curl --fail --location --show-error --silent --retry 3 --retry-delay 2 \ -o "${destdir}/${file}" "$url" \ && echo "Saved ${destdir}/${file}" \ || echo "Failed to download $url" done
Many of the illustrations used on this website are from them, including the bird on the about page and the ship in the footer.
Regarding about-page-bird, it is inserted using an org-mode
#+begin_export html block. It is a showcase of how powerful relying on
org-mode is, when one can directly describe things both in the light
syntax of org-down and at the same time directly access the export
language.
Embedding another file like this means that I do not have to keep track of if it is “up to date” in the same way as I do not have to keep the site configuration up to date with this documentation; this is the configuration!
From the repository:
Above all, I tried to make org-static-blog as simple as possible. There are no magic tricks, and all of the source code is meant to be easy to read, understand and modify.
Either by running C-u M-x org-static-blog-publish or calling it
through Emacs lisp like this (in the build script for example):
(org-static-blog-publish t)
Even if it is not a literal reference, but it may often be, especially when it comes to my academic writings.
Like in css, html, js, gnu et cetera
I can highly recommend using Caddy as your web server; you are going to want to use https and setting it up with nginx or Apache is just an annoying step to have to go through. Using Caddy as a reverse proxy is also stupidly simple.
