About this site
1 Overview
I originally started a blog about various programming-related things here (late 2000s), but I wanted more control over how the content is generated/stored, and so I created a different blog in 2013. This version used Hakyll as the static site generator. In 2026, I switched away from Hakyll to Lilac, a custom literate programming tool I developed, while retaining most of the original content as is. The source code of the latest version is also hosted here.
1.1 Hakyll vs Lilac
When we used Hakyll, we would generate the site, then commit all of the generated HTML and other static artifacts to a separate Git repo, and then host that on GitHub Pages.
We now use Lilac to generate the HTML. It is straightforward enough to use Lilac to write new content, but the Hakyll-era content still needs to be included in the tarball because there are old URLs out there that still reference it. See Old content.
2 Lilac configuration
name = "Linus Arver's Website"
homepage = "https://funloop.org"
repo = "https://github.com/listx/personal-website"
version = ""
nonTangledPathspecs = [
":(exclude,top).gitmodules",
":(exclude,top)*.org",
":(exclude,top)LICENSE",
":(exclude,top)art.md",
":(exclude,top)code/",
":(exclude,top)codex/",
":(exclude,top)favicon*",
":(exclude,top)file/",
":(exclude,top)img/",
":(exclude,top)post/2013-*.org",
":(exclude,top)post/2014-*.org",
":(exclude,top)post/2015-*.org",
":(exclude,top)post/2016-*.org",
":(exclude,top)post/2017-*.org",
":(exclude,top)post/2018-*.org",
":(exclude,top)post/2019-*.org",
":(exclude,top)post/2020-*.org",
":(exclude,top)post/2021-*.org",
":(exclude,top)post/2023-*.org",
":(exclude,top)post/2024-*.org",
":(exclude,top)post/2025-*.org",
":(exclude,top)rust-js/",
":(exclude,top)vendor/"
]
custom-html-head3 Publish locally
org_files := $(shell find . \
\( -path "./codex" \) -prune -o \
-name "*.org" -exec grep -l "^:ID: " {} +)
obj_files := $(org_files:%.org=%.lobj)
archive_file := site.larc
all: tangle weave lint
.PHONY: all
$(obj_files) &: $(org_files)
lilac compile $?
touch $(obj_files)
$(archive_file): $(obj_files)
lilac archive $^ --out-file $@
tangle: $(archive_file)
lilac tangle $< --out-dir .
touch tangle
weave: $(archive_file)
lilac weave $< --out-dir ./site --write-css --write-js
touch weave
lint: $(archive_file)
lilac lint $<
touch lint
Makefile:publish
gitconfig:
git config diff.orderfile .git-orderfile
.PHONY: gitconfig
update:
niv update nixpkgs --branch nixos-25.11
.PHONY: update
clean:
git -C codex reset --hard
rm -rf \
$(archive_file) \
$(obj_files) \
site.tar site.tar.gz \
site/*
.PHONY: clean4.1 Old content
Until we migrate all old posts (pre-2026) to use Lilac, we will have a body of work which we'll need to still publish the old way. We don't want to maintain the Hakyll build dependencies, so instead we just pull in the old statically generated repo, then we overwrite it with any content generated by Lilac. Then we add it to a branch and deploy to GitHub. That way the old content won't be broken (all of the old URLs will still work).
4.2 Makefile
site.tar.gz: $(archive_file) weave vendor/mathjax vendor/mathjax-font-font vendor/source-fonts
rm -f site.tar site.tar.gz
git -C codex reset --hard
cd codex && git submodule update --init --recursive
# 1 ←
fix-mathjax
fix-jquery
fix-google-fonts
# 2 ←
tar cvf site.tar -C site .
# 3 ←
find -L codex -path 'codex/deps/elisp/lilac/deps' -prune -o \
-type f \
-not -path '*/.*' \
\( -name '*.html' -o -name '*.css' -o -name '*.js' -o -name '*.svg' \) \
| tar rvhf site.tar --hard-dereference --exclude='codex/deps/elisp/lilac/deps' -T -
# 4 ←
tar rvf site.tar vendor
gzip site.tar
# 5 ←
git -C codex reset --hard
# 6 ←
vendor/mathjax vendor/mathjax-newcm-font &:
./vendor-mathjax.sh
vendor/source-fonts:
./vendor-source-fonts.sh
# 7 ←
PAGES_DIR := listx.github.io
publish: site.tar.gz
@test -d $(PAGES_DIR)/.git || { echo "Error: $(PAGES_DIR) is not a git repo." >&2; exit 1; }
tar -xzf site.tar.gz -C $(PAGES_DIR)
# The tarball excludes dotfiles, so add these back.
touch $(PAGES_DIR)/.nojekyll
cd $(PAGES_DIR) && \
git add -A && \
{ git diff --cached --quiet || git commit -m "update"; } && \
git push
touch publish2 publishes the main site.
3 publishes the https://git.sr.ht/~listx/codex repo.
Publishing the site just means extracting it to the PAGES_DIR 7. That directory is a Git repo which houses all of the static content to be hosted by GitHub Pages, so we commit any changes (if any) and push it up.
5 cleans up the submodule directories, because fixing the MathJax (by modifying their HTML files to refer to our vendored versions) results in modifying tracked files. This way we reduce the chance of accidentally commiting those changes to the main repo (because Git tracks whether a submodule is "dirty").
4.2.1 Fix MathJax
There was a brief period of time when we used SourceHut for hosting. SourceHut did not allow any use of a CDN (everything had to be self-contained), so we had to vendor everything. While this made things tricky, on the flip side everything is now self-contained, including MathJax sources.
We have to modify the HTML files to use a locally vendored version 1 via fix-mathjax, and also include MathJax dependencies in the tarball 4.
First we download MathJax using vendor-mathjax.sh
set -euo pipefail
MAIN_PACKAGE="mathjax"
FONT_PACKAGE="mathjax-newcm-font"
VERSION="4.1.2"
mkdir -p vendor
pushd vendor
download_tarball()
{
local package
local version
local url_prefix
local tarball
package="${1:-}"
version="${2:-}"
tarball="${package}-${version}.tgz"
url_prefix="${3:-}"
if [[ -d "${package}" ]]; then
echo "${package} directory already exists"
return 1
fi
curl -OL "${url_prefix}/-/${tarball}"
tar xzvf "${tarball}"
mv package "${package}"
rm "${tarball}"
}
download_tarball \
"${MAIN_PACKAGE}" \
"${VERSION}" \
"https://registry.npmjs.org/mathjax"
download_tarball \
"${FONT_PACKAGE}" \
"${VERSION}" \
"https://registry.npmjs.org/@mathjax/${FONT_PACKAGE}"Nowe we make the old content refer to this newer version in fix-mathjax.
find codex -path 'codex/deps' -prune -o \
-type f -name "*.html" \
-exec sed -i 's|https://cdn.jsdelivr.net/npm/mathjax@4.0.0-beta.4/tex-mml-chtml.js|/vendor/mathjax/tex-mml-chtml.js|g' {} +
find codex -path 'codex/deps' -prune -o -type f -name "*.html" -exec \
sed-inject-mathjax-font-codex {} +Below we modify the HTML files depending on their "flavor". The critical piece in each of the code blocks below is telling MathJax to load the fonts from the local /vendor/<<mathjax-font>>-font directory. If we don't do this, then MathJax still tries to load fonts from a CDN.
mathjax-newcmsed -i 's|output:[[:space:]]*{|loader: { paths: { "mathjax-font": "/vendor/mathjax-font-font" } },\
output: {|'4.2.2 Fix jQuery
Some of the pages pull in jQuery from upstream; we have to host these locally as well. They are vendored inside vendor, but we need to make some more sed replacements to make the relevant pages refer to the vendored version.
find codex -path 'codex/deps' -prune -o -type f -name "*.html" -exec \
sed-inject-local-jquery-codex {} +sed -i 's|src="https://code.jquery.com/jquery-3.6.4.min.js"|src="/vendor/jquery-3.6.4.min.js"|g'4.2.3 Fix Google web fonts
We have to host the Source family of fonts directly as well, instead of pulling them over the network from Google web fonts.
set -euo pipefail
url_sans="https://raw.githubusercontent.com/adobe-fonts/source-sans/3.052R"
url_serif="https://raw.githubusercontent.com/adobe-fonts/source-serif/4.005R"
url_code="https://raw.githubusercontent.com/adobe-fonts/source-code-pro/2.042R-u%2F1.062R-i%2F1.026R-vf"
mkdir -p vendor/source-fonts
pushd vendor/source-fonts
curl -OL "${url_sans}/WOFF2/VF/SourceSans3VF-Italic.otf.woff2"
curl -OL "${url_sans}/WOFF2/VF/SourceSans3VF-Upright.otf.woff2"
curl -OL "${url_serif}/WOFF2/VAR/SourceSerif4Variable-Italic.otf.woff2"
curl -OL "${url_serif}/WOFF2/VAR/SourceSerif4Variable-Roman.otf.woff2"
curl -OL "${url_code}/WOFF2/VF/SourceCodeVF-Italic.otf.woff2"
curl -OL "${url_code}/WOFF2/VF/SourceCodeVF-Upright.otf.woff2"
cat << EOF > source-fonts.css
@font-face{
font-family: 'Source Code Pro';
font-weight: 200 900;
font-style: normal;
font-stretch: normal;
src: url('SourceCodeVF-Upright.otf.woff2') format('woff2');
}
@font-face{
font-family: 'Source Code Pro';
font-weight: 200 900;
font-style: italic;
font-stretch: normal;
src: url('SourceCodeVF-Italic.otf.woff2') format('woff2');
}
@font-face{
font-family: 'Source Sans 3';
font-weight: 200 900;
font-style: normal;
font-stretch: normal;
src: url('SourceSans3VF-Upright.otf.woff2') format('woff2');
}
@font-face{
font-family: 'Source Sans 3';
font-weight: 200 900;
font-style: italic;
font-stretch: normal;
src: url('SourceSans3VF-Italic.otf.woff2') format('woff2');
}
@font-face{
font-family: 'Source Serif 4';
font-weight: 200 900;
font-style: normal;
font-stretch: normal;
src: url('SourceSerif4Variable-Roman.otf.woff2') format('woff2');
}
@font-face{
font-family: 'Source Serif 4';
font-weight: 200 900;
font-style: italic;
font-stretch: normal;
src: url('SourceSerif4Variable-Italic.otf.woff2') format('woff2');
}
EOFNow make the HTML content refer to the local fonts.
find codex -path 'codex/deps' -prune -o \
-type f -name "*.html" -exec \
sed-use-local-google-fonts {} +sed -i \
's|href="https://fonts\.googleapis\.com[^"]*"|href="/vendor/source-fonts/source-fonts.css"|g'4.2.4 Reference vendored assets
Finally we need to actually use these vendored assets by referring to them in every woven HTML file. That's what we do in custom-html-head.
[htmlHead]
onlyLocalImports = true
injection = """
<link rel="stylesheet" href="/vendor/source-fonts/source-fonts.css">
<script>
window.MathJax = {
loader: { paths: { "mathjax-newcm": "/vendor/mathjax-newcm-font" } },
output: { font: "mathjax-newcm" },
tex: {
tags: 'ams'
},
options: {
ignoreHtmlClass: 'verbatim|code'
}
};
</script>
<script id="MathJax-script" async="" src="/vendor/mathjax/tex-mml-chtml.js"></script>
"""5 Hakyll setup
Below is a record of how the old content used to be built with Hakyll. This is here mainly for historical interest. It will be removed after we've fully transitioned away to the new publishing step with Lilac.
# This Makefile is expected to be run inside a nix-shell.
PROJ_ROOT := $(shell git rev-parse --show-toplevel)
BLOG_ADDR?=localhost
BLOG_PORT?=8020
all: sync
.PHONY: all
# Check for broken links.
check:
cabal run -- blog check
.PHONY: check
gen-css:
cabal build -- base >/dev/null
cabal exec -- base
.PHONY: gen-css
# JavaScript generated from Rust.
gen-js:
rustup default stable
make -C rust-js build
.PHONY: gen-js
sync: build-site
./sync.sh
.PHONY: sync
cabal-update:
cabal update
.PHONY: cabal-update
build-binaries:
cabal build
.PHONY: build-binaries
build-site: build-binaries
cabal run -- blog rebuild
.PHONY: build-site
watch: build-binaries
cabal run -- blog watch --host $(BLOG_ADDR) --port $(BLOG_PORT)
.PHONY: watch
nixpkgs_stable_channel := nixos-24.05
update-deps: nix/sources.json nix/sources.nix
niv update nixpkgs --branch $(nixpkgs_stable_channel)
niv update
touch update-depsThe local listx.github.io folder used to have the Github repo which held the generated content. Hakyll would by default publish to _site, so the script below would sync these two with rsync.
dest='listx.github.io/'
targ='_site/'
if [[ ! -d $targ ]]; then
echo "\`$targ' directory missing"
exit 1
elif [[ ! -d $dest ]]; then
echo "\`$dest' directory missing"
exit 1
fi
rsync -ahP --no-whole-file --inplace --delete --exclude=.git\* $targ $dest6 Miscellaneous
*.hi
*.html
*.o
*.lobj
*.larc
*.tar
*.tar.gz
**/__pycache__/
code/2017-04-02-calling-c-from-haskell/hs/ffi
code/2021-03-15-bresenham-circle-drawing-algorithm/golden.yaml
code/toy/binary-search-c
code/toy/binary-search-hs
.direnv
site
listx.github.io
.srht-pages-token
update-deps
tangle
weave
lint
publish
vendor/vendor/*.js binary