Skip to content

Commit 128e631

Browse files
committed
Add build notes for email newsletter
1 parent e9b0366 commit 128e631

File tree

3 files changed

+99
-1
lines changed

3 files changed

+99
-1
lines changed
141 KB
Loading
Lines changed: 95 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,95 @@
1+
---
2+
title: "Build Notes: Where I add an email newsletter with an ElectricSQL admin app"
3+
date: "2024-01-30T22:08:41.640Z"
4+
---
5+
6+
I've been avoiding adding a newsletter to my blog for months now. It's an
7+
obvious thing to do and people ask for it now and then but it just seemed...
8+
hard.
9+
10+
Then it occurred to me yesterday why — adding integrations suck. The obvious
11+
ways to add newsletter support was to either use a dedicated newsletter service
12+
which takes care of most things or to use a lower-level email API
13+
and manually hook it up a bit. But either way, it'd mean reading a bunch of
14+
docs, getting confused for a while, and writing some tedious, probably difficult
15+
to debug, code.
16+
17+
What I realized though is there's another way to do this — I'm not trying to
18+
sign up 10s of thousands of people here, I can probably just do this manually.
19+
I can manually send out verification emails and manually send out each
20+
"newsletter" for each blog post I write just by copy/pasting the post and email
21+
addresses into gmail.
22+
23+
It's not particularly hard to send a few verification emails each day. But it
24+
is hard to send 100s of emails each day.
25+
26+
This is very "do things that don't scale" Y Combinator style — which is the
27+
right strategy both because you learn more by doing things manually e.g. I
28+
can perhaps turn verification emails into a bit of a conversation and you avoid
29+
expensive automation for "things that scale" as they have high fixed cost and
30+
you're better off avoiding them until you're sure you actually need them.
31+
32+
This is a good example of why problem decomposition is valuable. When I looked
33+
at the problem as a whole of a blog email newsletter, it seemed like I had to
34+
use a service. But when I split up the problem into three:
35+
36+
1. People want to subscribe
37+
2. I need to verify email addresses are real
38+
3. I need to send out the email newsletters to the current list of subscribers
39+
40+
I realized only the first one needed code at all (in theory people could just email
41+
me if they wanted added but that'd probably have an unacceptably low conversion rate)
42+
and the second two could be done manually.
43+
44+
The famous architect and theorist [Christopher
45+
Alexander](https://en.wikipedia.org/wiki/Christopher_Alexander) gave a nice
46+
process for decomposing problems in his book [Notes on the Synthesis of
47+
Form](https://www.amazon.com/dp/0674627512?tag=doriantaylor-20) (worth
48+
reading).
49+
50+
First, that "intractable complex problems are solved by breaking them down into
51+
simpler, more tractable ones".
52+
53+
Second, that "for a given problem there are ways to take it apart that are
54+
better than others, and indeed there is often a single, unique decomposition
55+
pattern that is objectively better than the rest."
56+
57+
And third, that "Alexander rather innovatively defined a 'design problem' as a
58+
network of interacting concerns, such that any two interconnected concerns must
59+
be satisfied in tandem. Since the entire network was connected, this meant the
60+
problem set couldn’t be broken down without severing certain connections.
61+
Alexander’s dissertation showed not only how this could be done, but why it
62+
should be done by severing the fewest connections possible."
63+
64+
(I borrowed this summary from this great post https://dorian.substack.com/p/at-any-given-moment-in-a-process)
65+
66+
The only thing connecting my decomposed three problems for my newsletter is
67+
the list of subscribers — so this decomposition satisfies Alexander's "sever
68+
the fewest connections possible" advice.
69+
70+
So with the mental block removed, I whipped out the code yesterday
71+
afternoon and this morning and I'm now the proud owner of the world's simplest
72+
email newsletter setup!
73+
74+
There's a form at the end of each post to add your email (feel free to take a
75+
bit of time to go add your email before finishing). This submits to a Gatsby
76+
function which writes out the data to a Postgres db & sends me a Slack
77+
notification so I can send the verification email.
78+
79+
This is all hooked up via [ElectricSQL](https://electric-sql.com/) to a
80+
one-screen admin app where I can see subscribers & get emails to send
81+
verification and newsletter emails.
82+
83+
![admin screenshot](./admin-screenshot.png)
84+
85+
All in all it took me about 3.5 hours to build — 2.5 hours to develop and 1
86+
hour to deploy (ran into some odd errors). There are [two
87+
tables](https://github.com/KyleAMathews/blog/blob/master/admin-app/db/migrations/001-create-tables.sql),
88+
[an 85-line admin
89+
component](https://github.com/KyleAMathews/blog/blob/master/admin-app/src/routes/index.tsx),
90+
and a [99-line API
91+
Function](https://github.com/KyleAMathews/blog/blob/master/src/api/register.ts).
92+
93+
It's fun having a blog admin now. I have no idea what else I'll do with it but
94+
now that it's there, it's trivial now to add other interactive bits to the
95+
site. We'll see if I think of anything.

src/templates/blog-post.js

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,7 @@ const { rhythm, scale } = typography
1111
function BlogPostRoute(props) {
1212
const [subscribeError, setSubscribeError] = React.useState()
1313
const [subscribeSuccess, setSubscribeSuccess] = React.useState()
14+
const [submitting, setSubmitting] = React.useState(false)
1415
const post = props.data.markdownRemark
1516

1617
let tags
@@ -90,6 +91,7 @@ function BlogPostRoute(props) {
9091
e.preventDefault()
9192
const form = e.target
9293
const formData = Object.fromEntries(new FormData(form))
94+
setSubmitting(true)
9395
fetch(`/api/register`, {
9496
headers: {
9597
Accept: "application/json",
@@ -98,6 +100,7 @@ function BlogPostRoute(props) {
98100
method: `POST`,
99101
body: JSON.stringify(formData),
100102
}).then(async (res) => {
103+
setSubmitting(false)
101104
if (!res.ok) {
102105
const body = await res.json()
103106
setSubscribeError(body.error)
@@ -127,7 +130,7 @@ function BlogPostRoute(props) {
127130
required
128131
style={{ marginRight: rhythm(0.25) }}
129132
/>
130-
<button type="submit">Subscribe</button>
133+
<button disabled={submitting} type="submit">Subscribe</button>
131134
</form>
132135

133136
<p

0 commit comments

Comments
 (0)