Newsletter¶
Any blog can also be a newsletter. Readers subscribe, confirm by email, and receive posts as issues.
Turn it on by editing the blog and ticking Newsletter enabled. A
Subscribe link then appears on the blog, and /blog/<slug>/subscribe/ starts
accepting signups.
Before your first send¶
Read this part. Everything below it is easy; this part is where newsletters actually go wrong.
Set your site URL. Every issue contains an unsubscribe link built from it.
If it still resolves to localhost, every link in the send is dead — and a
send cannot be recalled. The app refuses to queue in that state; set
SITE_DOMAIN (or SITE_URL) first.
Set up SPF, DKIM and DMARC for your sending domain. No amount of code substitutes for this. Without it a few thousand messages land in spam, and you will not find out from any log — only from readers who never got it.
Consider a separate mailer for bulk. The Mailer alias field picks a
second entry from your MAILERS setting, so a 5,000-recipient send doesn't
share an IP and domain reputation with your password-reset emails.
Send yourself a test copy. It is the only way to catch a rendering or header problem before subscribers see it.
How subscribing works¶
- A reader enters their address.
- They get a confirmation email. They are not subscribed yet.
- They click it, confirm, and receive a welcome email.
This is double opt-in, and it's on by default. It protects you from someone signing up an address they don't own, and it is what keeps a list deliverable.
The signup page says the same thing whether or not an address is already subscribed — otherwise the form becomes a way to test whether a given person reads your blog.
Sending an issue¶
From a post, use Send as newsletter — that creates a draft issue. Or create one directly under Blog → Issues with its own subject and body.
On the issue page:
- Send test copy to yourself.
- Send to subscribers — this freezes the content and the audience, and starts delivery.
From that moment the issue is a permanent record: editing the post later does not change what was sent, and the issue can no longer be edited.
Delivery runs in the background in batches. The page shows progress, and Stop sending halts what hasn't gone out yet. Copies already delivered cannot be recalled.
If some deliveries fail, Retry failed deliveries re-queues the ones worth retrying. It never re-sends to anyone who already received it.
Sending needs a worker running (
manage.py db_worker). Without one, an issue sits at "queued" and nothing happens —/smallstack/status/reports it.
Unsubscribing¶
Every issue carries a one-click unsubscribe button that mail clients show next to the sender's name, plus a link in the footer. Both work without the reader signing in, and the unsubscribe link in an old email keeps working forever.
Unsubscribing never deletes the record — that's how we remember someone opted out, so a later list import can't quietly add them back.
Bounces¶
If an address is rejected outright, it's suppressed and receives nothing further. A temporary failure (a dropped connection, a greylist) is retried and does not suppress anyone; only a run of consecutive failures eventually does. Staff can clear a suppression under Blog → Subscribers.
Importing a list¶
manage.py blog_import_subscribers <blog> list.csv --consent-note "..."
The consent note is required, because importing is the one path that skips
confirmation: you are asserting these people already agreed, and the record of
why needs to exist before the first send rather than be reconstructed
afterwards. Without --confirmed, imported addresses still have to confirm.
Don't import a list you bought, or addresses collected for something else. It is the fastest way to get a sending domain blocked.