The obligatory meta post
The current meta seems to be making personal websites. Everybody’s doing it and, if you are reading this, I am too. I hope this is the start of a lasting, healthy online presence.
Another trend I’m noticing with these online spaces is the tendency for the first (and often only) post to be about the site’s setup and such. Since I love talking about myself, here’s a little write up about this site’s current inner workings!
The server
First up, the hardware! This is probably the most boring part of the setup. My site is currently running on a shitty laptop sitting in my basement. The power cable is broken so if anyone even slightly nudges it, the computer shuts off instantly. Not exactly Production Quality 99.99% Uptime…
It would probably have been cheaper and easier to just rent one of those near-free VPSs somewhere but setting up this laptop was a pretty fun learning experience. Until then, I had never tried replacing the operating system on a computer. It was honestly pretty refreshing feeling like I was the master of the computer and not the other way around for a change.
The server is running behind a Cloudflare proxy to provide a basic level of security. I’ll refrain from further explanations of my networking stack due to some pretty glaring security issues which I’d rather not elaborate on…
NixOS
An old laptop running as a server isn’t that unusual. Much more unusual is the choice of operating system. Rather than opting for something like Ubuntu or Arch, I went with NixOS.
Both my ““server”” and my Macbook Pro have their configurations stored in a single monorepo. That approach definitely has its pros and cons: it’s nice being able to share overlays and packages between the two configurations but trying to reconcile NixOS and nix-darwin has proven to be quite a hassle. I definitely spent waaay more time than is reasonable figuring out how to manage such a monorepo, an issue that was not helped by Nix’s absolutely bonkers module system. Maybe I’ll talk more about my ambivalent thoughts on NixOS and the Nix ecosystem in some other post.
Once I had actually gotten NixOS configured and working, setting up the actual server was probably something like 7 LOC. Pretty simple, since running NGINX as a reverse proxy is a pretty common use case on NixOS1.
Furthermore, if I ever decide to actually switch to a proper VPS like I should’ve done from the start, I can just rebuild my NixOS config on that machine! Magical!
linus.onl
Finishing off my mega-scuffed config, I obviously couldn’t go with a well established SSG like Hugo or Jekyll. Instead, I decided to take some inspiration from Karl B. and write my own bespoke build script.
I decided to try using TCL for implementing this script, figuring the language’s “everything is a string” philosophy2 would make it an excellent shell-script replacement. While that definitely was the case, the script actually ended up not relying that much on external tools as it grew.
While exploring the language, I learned that where TCL really shines is in its
metaprogramming capabilities. I used those to add a pretty cool preprocessing
phase to my post rendering pipeline: everything between a <?
and a ?>
is evaluated as TCL and embedded directly within the
post. The preprocessor works in three steps. First it takes the raw markup,
which looks like this:
# My post
Here's some *markdown* with __formatting__.
The current time is <?
set secs [clock seconds]
set fmt [clock format $secs -format %H:%M]
emit $fmt
?>.
That markup is then turned into a TCL program, which is going to generate the
final markdown, by the
parse
procedure.
emit {# My post
Here's some *markdown* with __formatting__.
The current time is }
set secs [clock seconds]
set fmt [clock format $secs -format %H:%M]
emit $fmt
emit .
That code is then evaluated in a child interpreter, created with interp create
. All invocations of the
emit
procedure are then collected by
collect_emissions
into the following result:
# My post
Here's some *markdown* with __formatting__.
The current time is 02:46.
This is the final markup which is passed through a markdown renderer3 to
produce the final html. This whole procedure is encapsulated in
render_markdown
.
Embedded TCL is immensely powerfull. For example, the index and archive pages don’t recieve any special treatment from the build system, despite containing a list of posts. How do they include the dynamic lists, then? The list of posts that are displayed are generated by inline TCL:
# Archive
Here's a list of all my posts. All <? emit [llength $param(index)] ?> of them!
<?
proc format_timestamp ts {
return [string map {- /} [regsub T.* $ts {}]]
}
# NOTE: Should mostly match pages/index.md
emitln <ul>
foreach post $param(index) {
lassign $post path title id created updated
set link [string map {.md .html} $path]
emitln "<li>[format_timestamp $created]: <a href=\"[escape_html $link]\">[escape_html $title]</a></li>"
}
emitln </ul>
?>
And that code sample was generated inline too!! The code above is guaranteed to always be 100% accurate, because it just reads the post source straight from the file system.4 How cool is that!?.
I quite like this approach of writing a thinly veiled program to generate the final HTML. In the future I’d like to see if I can entirely get rid of the markdown renderer.
P.S. Here’s a listing of the site’s source directory. Not for any particular
reason other than that I spent 20 minutes figuring out how to get the
<details>
element to work.
Directory listing
linus.onl/
├── assets
│ ├── images
│ │ └── ahmed.jpg
│ └── styles
│ ├── normalize.css
│ └── site.css
├── pages
│ ├── about.md
│ ├── archive.md
│ └── index.md
├── posts
│ ├── first-post.md
│ ├── my-lovehate-relationship-with-nix.md
│ ├── second-post.md
│ ├── the-obigatory-metapost.md
│ └── third-post.md
├── Makefile
├── README.md
├── build.tcl
├── local.vim
└── shell.nix
Conclusion
All in all, this isn’t the most exotic setup, nor the most minimal, but it’s mine and I love it. Particularly the last bit about the build system. I love stuff that eat’s its own tail like that.
I hope this post was informative, or that you at least found my scuffed setup entertaining :)
-
Actually, my setup is a little longer because I use a systemd service to fetch and rebuild the site every five minutes as a sort of poor-mans replacement for an on-push deployment. Not my finest moment… ↩
-
Salvatore Antirez has written a great post about the philosophy of TCL. I highly recommend it, both as an introduction to TCL and as an interesting perspective on simplicity. ↩
-
Initially I was shelling out to smu but I switched to tcl-cmark because smu kept messing up multi-line embedded HTML tags. ↩
-
That does also mean that if the above sample is totally nonsensical, it’s because I changed the implementation of the archive page and forgot to update this post. ↩