This site now has an email newsletter. You can sign up at https://chard.ooo/subscribe to receive everything I publish here via email. New posts will be delivered to subscribers automatically the morning after they go live on the site.
I built the entire newsletter system myself. Aside from an integration with an email provider, the whole thing was coded and is hosted by yours truly. It’s a whole lot simpler than something I could have paid for, but by doing it myself I’ve maintained ownership over the whole thing and avoided another monthly hosting fee.
If all you care to know is that it’s possible to subscribe to my blog, then you can stop reading here! But if you’d like to learn a little more about why or how I put it together, see below.
Why I built it
My primary motivator to set up email subscriptions was a request from a friend of mine. He asked me ages ago if it was possible to get notified when new posts went live, and then stared at me blankly when I responded by telling him, “Sure, just point something at the RSS feed!”
I’m a very happy user of Miniflux, so when I first made this blog, a feed seemed like a perfectly reasonable solution for would-be subscribers. But I’ve since learned that I’m in a tiny little bubble, and in spite of the age and simplicity of XML syndication formats, most normal people have no idea what phrases like “feed reader” or “Atom” or “RSS” mean.
Anyways, this friend of mine is the only person I know who actually reads most of what I publish, so eventually I felt I owed him slightly better customer service than the stalwart indifference of an XML feed. That nagging feeling of obligation activated my next motivator, the sick little guy in my brain who sees something and immediately asks, “How hard could that really be?” Once that little goblin gets some momentum, there’s no stopping him.
And, in this case, the goblin turned out to be right: I already have a VPS that makes it easy to host my own software, there’s a slew of free-or-cheap email APIs out there, and tracking blog post deliveries does not require very complex code (especially if you don’t care about it being highly resilient or feature-rich). So I guess I also built it myself just to prove that I could.
Finally, and to the smallest extent, hosting my own email newsletter was about support and connection. I don’t collect any analytics from my site (this isn’t a strongly principled stance, I may change my mind in the future), so I currently have no idea how many people1 read anything I write. This doesn’t really matter, and the numbers are probably small enough to be embarrassing anyways, but I do get curious about it sometimes. I can think of two simple ways to track readership. One would be to install some invisible2 client-side tracking code on my site and track everyone who comes through. Another would be to give human readers the option to disclose their existence and engage more directly. Between the two, the second seems both more humane and more meaningful. It leaves the choice in the hands of the audience, gives me a tiny bit of insight into my audience’s size and makeup, and (for those who do subscribe) gives us the opportunity to stay connected down the line.
So if you have any interest in supporting this blog or wish to cast a vote in favor of me continuing to write, signing up for the newsletter is the best way to do that. Just knowing that anyone is out there who cares to read what I write is more of an encouragement than you might expect.
How it’s made
If you want to build an email newsletter, you need a few things: a source of truth for content, a way of storing subscribers and deliveries, mechanisms for subscribers to sign up and unsubscribe, and a scheduled process to actually send emails. It’s not too much to figure out, but it’s not exactly nothing, either.
The first problem was embarrassingly easy, because, as discussed above, my site already has an Atom feed. It’s a structured document with the URL and content of all my articles, and it gets updated every time I publish something new. It is such a sweet feeling to have a problem and then realize you already solved it months ago.
I also tried to keep state management simple. I’m already running a Postgres
instance on my VPS to store data for a few self-hosted utilities, so I just
needed to set up a new DB on that instance for this use case. This whole thing
can scrape by with two tables: subscribers holds email addresses and statuses
for my subscribers, and emailed_posts holds all the posts that have been
successfully delivered in the past. It could grow from here if necessary (eg, in
the future I may want to store delivery logs per subscriber instead of globally
per post), but I wager this’ll get me pretty far.
Now we get to the part where I had to write some actual code. For subscriber
management, I created an HTTP server in Go, and set it up under my domain to
serve a few new routes. One endpoint handles form submissions from my
subscribe page, one serves the subscription
confirmation3 page, and a final one handles opt-outs. All of these require a
proper server because they need to verify live data and possibly take action on
every request; unfortunately, you can’t cache side effects. The confirmation and
opt-out routes both verify subscriber-specific tokens which are provided in URL
query parameters. These tokens are generated randomly when new subscribers sign
up, and are the only real authentication tactic employed by this whole system.
All three of these endpoints return HTML documents to the caller, which are
rendered using html/template from the Go standard library. In the case of the
signup form, that document is a teeny tiny fragment which is swapped into the
page dynamically by HTMX, so the entire page doesn’t need
to refresh. I’m getting to the point where I’ll use HTMX just about anywhere I
can. It’s a delightful little technology.
The final piece of the puzzle is the mailer, a little bundle of code that pulls
my Atom feed, diffs its items against what’s already in the emailed_posts
table, then sends any new posts to all the (verified) email addresses it can
find in the subscribers table. Email bodies are HTML documents, and consist of
each post’s raw content plus a link to my unsubscribe route personalized with
the subscriber’s unique token. Emails are delivered using Resend, which I chose
for no good reason except that it has a free tier with limits that I don’t
anticipate exceeding for a long time. This was also all written in Go, which is
arguably a bad choice for such a simple script, but it allowed me to re-use a
bunch of library code I’d already written for other parts of my site.
That just left getting everything up and running. For the new website
functionality, I made sure Caddy, my reverse-proxy, would route all dynamic
requests (under the /app/* wildcard route) to my new server instead of looking
for static files with those names. Then I set up a
systemd service and timer on my VPS to execute the mailer
script once a day. Everything on my VPS is running in one big Docker Compose
stack, which makes deploying app or config changes pretty straightforward. I
don’t have any serious monitoring or alerting set up; all I’ve done is signed up
as my own first subscriber so I’ll know to start digging through logs if I don’t
get an email tomorrow morning.
I did all of the above without letting an LLM write any code for me, but I did use agents like Claude Code, Amp, and OpenCode to review my code as I was working on it. This is not a matter of pride or taste; any of the aforementioned agents would have done a better job than I did in less time than I took. I avoided agents in this case because I was in no rush, and saw no reason to burn the money. But I also wanted the result to feel hand-built. That is, I wanted to feel personally responsible for how the whole thing was put together, and to really know it from tip to tail. This site is a hobby, not a product. It’s personal software, and I want it to stay that way, at least for now.
-
The regular traffic reports I receive from Cloudflare are so inflated by bots that they’re entirely useless for a small-scale site like mine. ↩︎
-
Yes, sure, I could make it not-invisible. I could make it an opt-in thing, I could advertise it clearly, I could be extremely above board about the whole deal. But I’m afraid of privacy laws, afraid of annoying people, and afraid of JavaScript. ↩︎
-
Email verification is a little tedious, but the idea of a bad actor being able to subscribe someone else to my newsletter makes me feel quite yucky. So, probably forever, you will have to prove that you actually own the email you submitted in order to subscribe to my newsletter. ↩︎