… it was easy and then it became increasingly complex since the early 2000s
PDFs and Node in 2025: A Comprehensive Guide to Rendering and Parsing
Exploring the current state of PDF libraries in Node.js, comparing capabilities for creating new PDFs and parsing existing ones
Migrating an old static HTML site to Hugo
Today I embarked on migrating my old HTML website to Hugo, encountering various challenges and learning valuable lessons about static site generation.
AppImages and GNOME desktop launchers
AppImages are how a lot of apps are distributed on Linux these days but there isn’t an established “install this AppImage so I can launch it” pattern.
Install this AppImage so I can launch it (GNOME edition)
- Download your favourite AppImage from a trusted publisher
- Move the file to
/opt/<my app>/<app>.AppImage - Make the AppImage executable
chmod +x /opt/<my app>/<app>.AppImage - Download an icon for your app, or extract the app icon from the AppImage
and put it in
/opt/<my app>/<app>.png - Create a GNOME Desktop Launcher file in
~/.local/share/applications[Desktop Entry] Name=My App Comment=A Description Exec=/opt/<my app>/<app>.AppImage --no-sandbox Icon=/opt/<my app>/<app>.png Terminal=false Type=Application Categories=Utility; - Update GNOME’s desktop file entries:
sudo update-desktop-database - Open your app
Continued; filtering Drupal comment spam using an LLM
A continuation of my previous post on Filtering Drupal comments for spam
Overview
- Old Drupal 5 blog with my old posts stuck in the Drupal
nodetable - ~19k comments on my pages before I fixed spam filtering
- Keeping it local with Ollama
- Google Gemma3 was a good speed/quality for detecting spam
- Deepseek R1 takes way too long because it reasons for every comment
TLDR:
- I excluded a lot of spam comments with Gemma3 and boiled them down to a more manageable set
- Automating this process means I could easily miss comments due to false-positives
- Need to log the Drupal node ID so that I can link filtered comments back to my markdown blog posts
Certbot Renewal with Docker
A hacky way to get it done
docker-compose.yml
certbot:
image: certbot/certbot:latest
depends_on:
- nginx
command: >-
renew -v --webroot --webroot-path=/var/www/certbot --agree-tos --email "my-email"
volumes:
- ./certbot/www/:/var/www/certbot/:rw
- ./certbot/conf/:/etc/letsencrypt/:rw
Run:
docker compose up certbot
Filtering ancient Drupal comments for spam using an LLM
I have been meaning to resurrect my blog for a long time. I’ve written many versions of this post, whether on dev.to, wordpress.org, Obsidian Publish, etc. etc. etc. I have spent hundreds of dollars on the question “how do blog?!” and I’m back to “Pay $5/mo for a VPS, put HTML files on the internet.”
I have an old Drupal 5 blog from ~2014 which I migrated my ~2010 Drupal 4 blog posts from. I really liked the 2014 era content from when I first started experimenting with software defined radio and had some moderately well-commented posts on GQRX SDR and Linux gaming at the time.
Google Play Music & Gnome Shell Media Buttons
I have been trying to figure out how to use the media keys on my keyboard to control Google Play Music for a while. It turns out that Chrome is set up to listen for the media keys, but the X11/Gnome were capturing these events and not propagating them to the browser.
TLDR: Disable the Play/Pause, Next, and Previous keyboard shortcuts in the Gnome Keyboard controls.

Let’s Encrypt!
I’ve finally completed my goal of securing https://marcinkowski.ca and https://analytics.marcinkowski.ca using Let’s Encrypt. The process is easier than setting up the pretty URL rules for Drupal in nginx.
I followed Digital Ocean’s setup instructions found here: https://www.digitalocean.com/community/tutorials/how-to-secure-nginx-with-let-s-encrypt-on-ubuntu-14-04
Setting up certificate renewals
See my next article about setting up automatic certificate renewals:
TLDR:
Prerequisites:
- Your user needs to have write access to your webroot
- I have installed
certbot-autoin to/opt/certbot
- Get
certbotfrom the EFF (https://certbot.eff.org/) - Configure nginx to allow access to
http(s)://domain.com/.well-known/*:/etc/nginx/letsencrypt-authorization.conf: location ~ /.well-known { allow all; } /etc/nginx/enabled/domain.ca: include letsencrypt-authorization.conf - Create a cert using
certbot-autoscript:/opt/certbot/certbot-auto certonly -a webroot --webroot-path=PATH/TO/WEBROOT -d domain.ca - Add my new letsencrypt certificate and private key to the config file for this domain:
ssl_certificate /etc/letsencrypt/live/domain.ca/fullchain.pem; ssl_certificate_key /etc/letsencrypt/live/domain.ca/privkey.pem; - Followed the rest of the Digital Ocean article:
a. Generate a Strong Diffie-Hellman Group:
sudo openssl dhparam -out /etc/ssl/certs/dhparam.pem 2048
b. Add additional SSL params to use modern practises, including HSTS (Thanks Digital Ocean) ~~~
Let’s Encrypt Renewal
In my last article I TLDR’d how I got Let’s Encrypt set up for https://marcinkowski.ca and https://analytics.marcinkowski.ca.
Getting automatic renewals set up using certbot is pretty straightforward. I collected the following from a couple of places, including the github:certbot/docs/using.rst#Renewal
Add a twice daily cron entry for certbot to check for renewals:
30 6/18 * * * /opt/certbot/certbot-auto -q renew --post-hook "service nginx reload"
The --post-hook "service nginx reload" will reload Nginx’s config only if a certificate renewal is performed. You can check on this by adding -v to the call to /opt/certbot/certbot-auto and see the logging output stating that no renewals were attempted.