Skip to content

Commit 1b94521

Browse files
committed
add timestamp updater, update readme with resender, first blog post!
1 parent 7c94662 commit 1b94521

File tree

6 files changed

+113
-1
lines changed

6 files changed

+113
-1
lines changed

README.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
# viruus.zip
22

3-
Personal website and blog built with [Hugo](https://gohugo.io/) and the [hugo-bearblog](https://github.com/janraasch/hugo-bearblog/) theme.
3+
Personal website and blog built with [Hugo](https://gohugo.io/) and the [hugo-bearblog](https://github.com/janraasch/hugo-bearblog/) theme. Uses [resender](https://github.com/dropalltables/resender) for emails.
44

55
## Setup
66

content/blog/email-is-hard.md

Lines changed: 64 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,64 @@
1+
+++
2+
title = "Email Is (Very) Hard"
3+
date = "2025-11-12T16:47:25-08:00"
4+
description = "My move from Loops to a custom-built Resend-based solution."
5+
draft = "false"
6+
tags = ["email","hugo","programming","js","html","css","web"]
7+
+++
8+
#### **TL;DR** I made replaced Loops with a custom resend-based solution. I opensourced it and called it [resender](https://github.com/dropalltables/resender).
9+
10+
Email may be the most complicated thing ever to come out of computing. It is the only decentralized standard to ever truely thrive, and due to that it has to be backwards-compatible with mainframes in universities from 1971.
11+
12+
But I don't want to talk about email, mainly because I got banned from email.
13+
14+
<img src="/images/email-is-hard/loops-ban.png" alt="Loops support conversation" style="width: 50%;" />
15+
16+
[Loops](https://loops.so) is a SaaS for email. They do product, transactional, and marketing emails and they are backed by [Y Combinator](https://en.wikipedia.org/wiki/Y_Combinator). They're great! But my website isn't exactly good for email reputation, so I got (respectfully) banned.
17+
18+
This meant I had to I move from Loops, to something else. That something else is Resend-powered solution with (mostly) custom branding, for free.
19+
20+
Here's the spec:
21+
22+
The solution should be free, I should not need to pay for a replacement to a free solution. The solution should run in the background, I should not need to think about it, it needs to run in the background, intelligently and without causing everything to explode if something goes wrong. The solution should replace Loops completely, I do not want to compromise due to this move, it should auto-publish new blog posts to email, it should use a simple HTML form, and it should have double opt-in[^1].
23+
24+
To do this I first had to find an email provider. I've used [Resend](https://resend.com) before with [Coolify](https://coolify.io) to send status emails, but never as a "customer-facing" system.
25+
26+
Resend is very, very different from Loops. Instead of a pre-built solution, they are literally a Amazon SES wrapper with some basics. This means that for free you get:
27+
28+
- A 1000-person audience, with segments
29+
- 3000 emails/month, 100/day
30+
- Sceduled broadcasts to specific segments
31+
- Unsubscribe pages
32+
- full API access for all features
33+
34+
This is almost the perfect solution for me, I don't have to host a database, or suffer through any of the SMTP hell, but I control what is in the emails, I control when people are added to the audience, I control the entire flow.
35+
36+
## A free[^2] solution
37+
38+
To send emails, the first thing I need to do is get emails. Because I had decided that my website would have zero Javascript, this meant my only option was HTML forms. Resend does not support HTML forms, but they do have a very simple JSON API. To convert between the private API and tokens and the HTML form, I needed a translation layer.
39+
40+
[Cloudflare Workers](https://workers.cloudflare.com/) was the solution. They provide free compute (albeit a very small amount), in the form of a customized express server. I just setup a secret for the Resend API key, and it worked.
41+
42+
My arcitecture works like this:
43+
44+
<img src="/images/email-is-hard/cf-workers.svg" alt="Cloudflare Workers flowchart" style="width: 50%;" />
45+
46+
## Background Tasks
47+
48+
To make everything run in the background, any staic generation works through Hugo render hooks, anything related to the Worker just wakes on API call, and anything that is linked to git commits is a Github action triggered by a push. Phew, that was quick!
49+
50+
## A Complete Replacement
51+
52+
To replace Loops, my solution needs to both email people when a new blog post is deployed and it needs to have double opt-in.
53+
54+
To send out emails when a now blog post is posted, a Github Action follows this procedure:
55+
56+
<img src="/images/email-is-hard/github-action.svg" alt="GitHub Action flowchart" style="width: 50%;" />
57+
58+
To make double opt-in, my Cloudflare Worker sends out an email and stores thei data in a KV Store for 24 hours, if they click the link sent to their email, it adds them to the audience, otherwise it deletes their info after 24 hours. This makes sure that the worker is never under too much load, and that anything in it is ephemeral.
59+
60+
I am calling this solution [resender](https://github.com/dropalltables/resender), and I'm opensourcing it! If you want to use it just follow the instructions in the README, all you need is a Cloudflare and Resend account.
61+
62+
[^1]: Double opt-in is a email-marketing strategy where you send a transactional email to a subscriber to have them confirm their subscription, showing their email server that they really do want to recieve your emails, while at the same time reducing your audience to true subscribers.
63+
64+
[^2]: Mostly free, it maxes out at 3000 emails/month, and 1000 subscribers, Cloudflare also has worker limits.

datedooter.sh

Lines changed: 40 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,40 @@
1+
#!/bin/bash
2+
3+
# datedooter.sh - Update blog post timestamp to current time
4+
# Usage: ./datedooter.sh <post-name>
5+
# Example: ./datedooter.sh email-is-hard
6+
7+
set -e
8+
9+
if [ $# -eq 0 ]; then
10+
echo "Usage: $0 <post-name>"
11+
echo "Example: $0 email-is-hard"
12+
exit 1
13+
fi
14+
15+
POST_NAME="$1"
16+
POST_FILE="content/blog/${POST_NAME}.md"
17+
18+
# Check if file exists
19+
if [ ! -f "$POST_FILE" ]; then
20+
echo "Error: File '$POST_FILE' not found"
21+
exit 1
22+
fi
23+
24+
# Generate current timestamp in ISO 8601 format with timezone
25+
# Format: 2025-11-11T19:18:11-08:00
26+
TIMESTAMP=$(date +"%Y-%m-%dT%H:%M:%S%z" | sed 's/\([+-][0-9][0-9]\)\([0-9][0-9]\)$/\1:\2/')
27+
28+
echo "Updating timestamp in $POST_FILE"
29+
echo "New timestamp: $TIMESTAMP"
30+
31+
# Update the date field in the frontmatter (macOS sed requires -i '' for in-place editing)
32+
if [[ "$OSTYPE" == "darwin"* ]]; then
33+
# macOS
34+
sed -i '' "s/^date = \".*\"$/date = \"$TIMESTAMP\"/" "$POST_FILE"
35+
else
36+
# Linux
37+
sed -i "s/^date = \".*\"$/date = \"$TIMESTAMP\"/" "$POST_FILE"
38+
fi
39+
40+
echo "Done."

static/images/email-is-hard/cf-workers.svg

Lines changed: 4 additions & 0 deletions
Loading

static/images/email-is-hard/github-action.svg

Lines changed: 4 additions & 0 deletions
Loading
87.5 KB
Loading

0 commit comments

Comments
 (0)