DigressEd.net
Occasional Digressions
Emacs + Elixir Config
Playing with Elixir + Phoenix and had to set up my emacs conf. There are many guides but many seem out of date and with too many complicated steps and options. I chose,
- Native
elixir-ts-mode(as of Emacs 30.1) - Configured with eglot instead of lsp.
My emacs setup is configured with Jack Rusher's emacs
config cloned to ~/.emacs.d, and my own config (ep-local.el) from my repo of
personal dot-files into ~/.emacs.d/lisp/. Jack has configured things nicely to let you extend his own config.
Instructions
On macOS, install elixir and elixir-ls (I also installed tree-sitter-cli but it might not be needed since we'll install the grammars from within emacs.
brew install elixir elixir-ls
In ep-local.el, I added:
;; elixir dev
(require 'eglot)
;; This is optional. It automatically runs `M-x eglot` for you whenever you are in `elixir-mode`
(add-hook 'elixir-mode-hook 'eglot-ensure)
(add-to-list 'eglot-server-programs `(elixir-mode "/opt/homebrew/bin/elixir-ls"))
;; If you access any projects via symlinks, and the lsp crashes immediately on startup in those projects, you might need this:
;;(setq find-file-visit-truename t)
;; built-in as of emacs 30.1
(require 'elixir-ts-mode)
;; https://github.com/ananthakumaran/exunit.el
(use-package exunit
:config
(add-hook 'elixir-mode-hook 'exunit)
;; Optionally configure `transient-default-level' to 5 to show extra switches
;; Or use `C-x l' to change the level of individual commands and switches
;; NOTE: default is 4
(setq transient-default-level 5))
;; https://github.com/J3RN/inf-elixir
(use-package inf-elixir
:bind (("C-c i i" . 'inf-elixir)
("C-c i p" . 'inf-elixir-project)
("C-c i l" . 'inf-elixir-send-line)
("C-c i r" . 'inf-elixir-send-region)
("C-c i b" . 'inf-elixir-send-buffer)
("C-c i R" . 'inf-elixir-reload-module)
("C-c C-z" . 'previous-multiframe-window)))
;; amend your auto-mode-alist:
(setq auto-mode-alist
(append
'(
;; ...
("\\.ex\\'" . elixir-ts-mode)
("\\.heex\\'" . elixir-ts-mode)
;; ...
)
auto-mode-alist))
With the configuration in place, launch Emacs and run
M-x treesit-install-language-grammar
You'll need to do it twice,
- for
elixirusinghttps://github.com/elixir-lang/tree-sitter-elixiras the source repo, - for
heex(for Phoenix) usinghttps://github.com/phoenixframework/tree-sitter-heex.
I used default for all other prompts, but this probably assumes you have a dev setup already configured.
Published: 2025-09-30
Stubble & Co 20L Hybrid backpack
I've owned a Brenthaven 15/17 backpack for -- what feels like -- close to 20 years. I had to search for it and found a French review for it dated Dec 2008. This thing is a tank and has served me well including hundreds of work and personal trips. It met all my needs for 2008 me: lots of pockets, large laptop pocket, mesh pockets, zippered pockets, comfortable to wear.
2025-me, however, has been needing something more practical for current needs. For one, laptops are no longer thick as they were back then, plus as bulky as it was and, with as many pocket as it has, it is just not super practical for a 2-day work trip or even a morning starting at the gym. So a couple of months ago I started researching bags and immediately got overwhelmed by all the choices and abandoned several opened tabs in a browser window somewhere. Until last week, when I decided to bite-the-bullet.
Stubble and Co's lineup drew my attention, and I was initially looking at the Hybrid 30L, which seemed great for exactly those things I needed but also seemed a bit big - one of the things I got tired of about the Brenthaven one. So I started looking at the Everyday 20L which looks so nice with all the organizing and seemed about the right size for my height (176cm) and was set to buy it when I noticed a 20L version of the Hybrid got introduced. The problem is there are no reviews for it, since it's so new. And also, the photos on the site don't show as much detail of what the inside looks like compared to the photos on the page for the 30L.
After discussing with my partner (a many-bagger), she suggested I go with the clamshell opening for convenience. I spent two more nights looking at photos, even considered ordering both to compare and returning one, but finally decided to go with the hybrid 20L (Sand color option) along with the Wash bag to replace my not-very-useful toiletry packs.

So far, I'm very happy with it. Will it last 20 years as well? Maybe not. But many people seem to be traveling with the 30L and say the quality is great. Also I probably won't be hauling mine everywhere like I did with the Brenthaven as I don't travel for work like I used to.
The main downsides are:
- Not as many pockets as the 30L, which has two big mesh pockets on the clamshell lid, one which is accessible from the outside.
- No mesh pockets period. There's only one zippered pocket inside and one unzippered in the laptop side.
- No key clips. This was also the case on the 30L. My brain seems to think I'm really going to miss that from the Brenthaven (and basically, every other backpack I've used).
- No compression strap, like on the 30L. Not sure if I'll miss that yet.
That said, so far, it feels like I made the right choice to go with the 20L. And maybe I just had too many pockets before because I did find a bunch of crap in my old bag that I didn't throw in the new one. The outside zip pocket works for flat quick-access items (work badge, phone charging cable, masks).

I can fit the laptop charger and cable, and a small stand for my phone in the zippered pocket in the main compartment:

This photo is to show how big the wash bag is, compared to my small Tech pouch and over-the-ear headphones.

I tested packing as I would for a 2-day trip:

And also for a morning session at the gym with shoes, change of clothes, workout hand-towel:

And a shower towel on top:

Loosening the compression straps to close it up:

The Brenthaven didn't have a bottle sleeve but this one does, which fits my 500ml bottle nicely:

And, of course, laptop (14") in there too:

This is already far better than before as I would carry the Brenthaven and a duffel-bag with all my gym gear in there.
The pocket in the laptop compartment is small:

The Brenthaven had a rigid sleeve pocket that kept papers nice and flat. I'll have to get a folder to throw in there as an alternative.
Finally, for any trips where I'll need to bring a carry-on, this is also much more convenient:

I've already taken it on a bike to the hardware store to pick up some bulbs and other things for the house.
Published: 2025-09-07
NixOS/nginx/acme with mutliple vhosts
(TL;DR: skip to the config)
I recently migrated my VPS from arch to nixos which also meant migrating my apache2 conf to nginx since it's what seems to be most commonly used. I got 90% of the way with my various domains (all of static content) but the last bit took a lot of patience as I couldn't find examples that handled all my cases:
- domain 1 (this blog):
- http redirected to https.
www.redirected to root domain, hosting the about page.- pages served from a separate location for the
blog.subdomain. - a redirect for another subdomain to an external site.
- domain 2, another personal site, nothing unexpected:
- http redirected to https.
www.redirected to root domain.
- domain 3, a domain not hosted on the VPS, just a redirect to an external site.
- needs http and https redirected to the external site.
I thought tweaking the conf a bit would be trivial but, while trying suggestions from other posts and even NixOS's Wiki all I ended up doing was :
- arbitrary subdomains redirected to another vhost,
- broken https redirects,
- getting rate-limited by Let's Encrypt because I didn't realize the implications of changing the acme config (newbie error).
I got all frustrated and commented out the entire acme / nginx config and focusing on other things while the Let's Encrypt restriction passed.
I determined the main source of my problems were the catch-all vhosts suggested on the Wiki and other places plus some missing forceSSL = true which turns out are really needed. They catch-all hosts seem ideal but it is not straightforward to follow as soon as you have different sub-domains that should not all be redirected to the same base host. I finally started re-enabling one by one, using staging certificates, and got it all working. Surely it could be simplified but that's for another time.
Sample Config
# set up files / permissions
systemd = {
# for nginx to write logs
services.nginx.serviceConfig.ReadWritePaths = [ "/var/log/nginx" ];
# where all files for the sites are in the filesystem - referenced
# later in the nginx config
tmpfiles.settings = {
www = {
"/var/www/domain-1.net" = {
f = { user = "<your user id>"; group = "nginx"; };
};
"/var/www/domain-2.com" = {
f = { user = "<your user id>; group = "nginx"; };
};
};
};
};
# let's encrypt certificates
security = {
acme = {
acceptTerms = true;
defaults = {
email = "<your contact email>";
group = "nginx";
# Be aware there are rate-limits so too many changes to your
# certs config may result in rejected requests for 24-hours.
# Instead, uncomment the server option to enable use of the
# staging environment
#server = "https://acme-staging-v02.api.letsencrypt.org/directory";
#
# If you need to force-regenerate certificates, you can do so
# with:
# sudo systemctl clean --what=state acme-domain-1.net.service
#
};
certs = {
"domain-1.net" = {
webroot = "/var/lib/acme/acme-challenge";
# list all subdomains that must share the certificate
extraDomainNames = [ "www.domain-1.net" "sub.domain-1.net" "redir.domain-1.net" ];
};
"domain-2.com" = {
webroot = "/var/lib/acme/acme-challenge";
extraDomainNames = [ "www.domain-2.com" ];
};
"domain-3.com" = {
webroot = "/var/lib/acme/acme-challenge";
extraDomainNames = [ "www.domain-3.com" ];
};
};
};
};
# finally the web server config
services.nginx = {
enable = true;
logError = "stderr emerg";
package = pkgs.nginxStable.override { openssl = pkgs.libressl; };
virtualHosts = {
domain-1 = {
serverName = "domain-1.net"; # matches domain used in security.acme.certs
forceSSL = true; # needed to handle http://domain-1.net -> https://domain-1.net redirection
enableACME = true;
root = "/var/www/domain-1.net/html"; # where the site's files are found
# acme path for request validation. `root` must match
# `webroot` in security.acme.certs.<domain>
locations."/.well-known/acme-challenge" = {
root = "/var/lib/acme/acme-challenge";
};
extraConfig = ''
charset utf-8;
# error_log /var/log/nginx/domain-1_error_log emerg; # uncomment if you want separate error log file
access_log /var/log/nginx/domain-1_access_log;
'';
};
# www version of domain-1.net. Needs the acme location for cert
# validation. All other requests are dedirected to https://domain-1.net/<path>
"www.domain-1.net" = {
serverName = "www.domain-1.net";
forceSSL = true;
useACMEHost = "domain-1.net";
locations = {
"/.well-known/acme-challenge" = {
root = "/var/lib/acme/acme-challenge";
};
"/" = {
return = "301 https://domain-1.net$request_uri";
};
};
};
# separate subdomain served from a different location
blog = {
serverName = "blog.domain-1.net";
forceSSL = true;
useACMEHost = "domain-1.net";
root = "/var/www/domain-1.net/blog";
locations."/.well-known/acme-challenge" = {
root = "/var/lib/acme/acme-challenge";
};
# optional, if you want separate logs
extraConfig = ''
charset utf-8;
# error_log /var/log/nginx/blog_domain-1_error_log emerg; # uncomment if you want separate error log file
access_log /var/log/nginx/blog_domain-1_access_log;
'';
};
# separate subdomain, redirected to external host
sub = {
serverName = "sub.domain-1.net";
forceSSL = true;
useACMEHost = "domain-1.net";
locations = {
"/.well-known/acme-challenge" = {
root = "/var/lib/acme/acme-challenge";
};
"/" = {
return = "301 https://externalhost.ext;
};
};
};
# second vhost, with only www. and non-www. versions. www. is
# redirected to non-www. Defines aliases for resources outside
# of root path
domain-2 = {
serverName = "domain-2.com";
forceSSL = true;
enableACME = true;
root = "/var/www/domain-2.com/html"; # / is served from .../html/ path
locations = {
"/.well-known/acme-challenge" = {
root = "/var/lib/acme/acme-challenge";
};
# point to a resources/ not within /html/
"/css/" = {
alias = "/var/www/domain-2.com/resources/css/";
};
"/js/" = {
alias = "/var/www/domain-2.com/resources/js/";
};
# redirect domain-2.com/info to domain-2.com/about
"/info" = {
return = "301 https://domain-2.com/about/";
};
};
extraConfig = ''
charset utf-8;
# error_log /var/log/nginx/domain-2_error_log emerg; # uncomment if you want separate error log file
access_log /var/log/nginx/domain-2_access_log;
'';
};
# www version of domain-2.com. Needs the acme location for cert
# validation. All other requests are dedirected to https://domain-2.com/<path>
"www.domain-2.com" = {
serverName = "www.domain-2.com";
forceSSL = true;
useACMEHost = "domain-2.com";
locations = {
"/.well-known/acme-challenge" = {
root = "/var/lib/acme/acme-challenge";
};
"/" = {
return = "301 https://domain-2.com$request_uri";
};
};
};
# a third domain - No files are hosted.
# only here to handle ssl certificate and redirect both
# domain-3.com and www.domain-3.com to an external site (e.g., soundcloud)
"domain-3.com" = {
serverName = "domain-3.com";
serverAliases = [ "www.domain-3.com" ];
forceSSL = true;
enableACME = true;
locations."/.well-known/acme-challenge" = {
root = "/var/lib/acme/acme-challenge";
};
locations."/" = {
return = "301 https://soundcloud.com/myband";
};
};
};
};
Published: 2024-09-07