personal website @
Switch branches/tags
Nothing to show
Clone or download
Fetching latest commit…
Cannot retrieve the latest commit at this time.
Failed to load latest commit information.


This is the latest iteration of my website, written in Crystal.


Markdown files in a Git repository are used to produce the archive pages. Archive posts are sorted based on the filenames, so they are prepended with dates in YYMMDD format and don't contain spaces. Filenames prepended with underscores are not rendered, providing a way to unpublish content or choose which content is ready to be published at any given time. Dates displayed alongside the titles are simply the first line in the file, while the titles are extracted from the second line.

Changes to the markdown files on the server are automatically detected, causing those pages to be rerendered as necessary. This allows for a single git push to publish content to the website, after committing the changes.


Convenient place to push files that need to be temporarily made available publicly, without having to take the website offline and spin up a static fileserver instead. While this functionality is basic, it often obviates the need for external services, and comes in particularly useful for things like making presentation resources available. Accessible at /temp, although not directly linked to from anywhere on the website.


Simplistic request count statistics, intended to give some hint about which pages are getting traffic, without resorting to third-party analytics services. Query parameters are not tracked, nor are paths that get routed to 404 errors, for security reasons. The data is saved to the filesystem in JSON format so that it gets retained across server restarts. Accessible at /stats, although not directly linked to from anywhere on the website.


Custom logger middleware for keeping persistent logfiles, since otherwise they disappear according to the scrollback settings, which is occasionally unhelpful. Logging statements are written simultaneously to STDOUT and an in-memory buffer, the contents of which are periodically appended to a logfile. This approach has the benefit of avoiding the overhead of constant opening and closing of files in the request pipeline hot-path; slow filesystem IO is minimized.


  • install Crystal
  • shards install
  • ln -s ~/archive archive && mkdir temp
  • crystal run src/ -- --dev


These deployment steps assume that certificates have already been obtained from Let's Encrypt (see for details on that process), that the redirect server has been setup, that the domain has been registered (through Namecheap or some other registrar), and finally that the records are set in the networking management section within DigitalOcean. For reference, the required networking records are documented in, since they're not very obvious and are of the frequent stumbling blocks people hit trying to initially deploy websites.

# clone and build on the server
sudo dnf install crystal git openssl-devel
git clone
cd website
shards install
crystal build --release --no-debug src/
# setup bare repository, hook, and symlink on the server
mkdir archive.git
cd archive.git
git init --bare
echo '#!/bin/sh' > hooks/post-receive
echo 'GIT_WORK_TREE=$HOME/archive git checkout -f' >> hooks/post-receive
chmod +x hooks/post-receive
mkdir ../archive
cd ~/website && ln -s ~/archive archive
# setup local repository to push to the server
git remote add pserver ssh://pserver/~/archive.git
git push -u pserver master
# must be ran with sudo for access to port 443
sudo ./website


  • occasionally stops working, with unhelpful SSL errors
  • update local and server setup to use Nix
  • deprecate running in detached tmux sessions
  • consider adding support for images
  • consider adding support for tags
  • migrate archive posts across