Hosting My Own Newsletter

Matthias Endler

自行架設我的電子報

我在這個部落格上經營電子報好幾年了,但有很長一段時間一封信都沒寄過。這是我如何終於讓它重新上線的故事,以及一路上學到的事。

先快速說明一下,因為這點曾造成一些誤會:所謂的「自行架設」,指的是我沒有使用電子報平台。註冊的後端和用來發送每期內容的 CLI 都是我自己做的,而每期內容本身就只是 git 儲存庫裡的 markdown 檔案。我仍使用Plunk作為發送後端(所以 SES、bounces(退信)、suppression lists(抑制名單)和取消訂閱頁面就不是我要煩惱的問題)。Plunk 本身是開源的,我也可以自行架設,但 deliverability(寄達率)那一端有太多邊界狀況,我很樂意付費讓別人來處理。🙃

Tinyletter 時期

舊版 Tinyletter 首頁,如今只剩令人感傷的 404。
舊版 Tinyletter 首頁,如今只剩令人感傷的 404。
來源:Wayback Machine

好幾年來,我的設定就是在網站上放一個小小的表單,指向 Tinyletter,一個專注於服務寫作者的小型電子報服務。我喜歡它的簡單。我從來不需要去想電子郵件的 deliverability、bounce rates(退信率)、suppression lists、SPF、DKIM、DMARC 或任何這類東西。我寫好東西,按下發送,大家就收到了。

Tinyletter 的撰寫頁面,展現介面的簡潔。
Tinyletter 的撰寫頁面,展現介面的簡潔。

它就是能用。然後 Tinyletter 關站了。

一點歷史背景:Tinyletter 由 Philip Kaplan(菲利普·卡普蘭) 於 2010 年打造,據說是在2010 年 10 月 31 日那個週日一天之內寫出來的

一年後它被 Mailchimp 收購,並悄悄成為想經營個人電子報、又不想煩惱什麼漏斗、分眾或 A/B 測試的寫作者們的實際首選。

然後在 2023 年底,Mailchimp(現在是 Intuit 旗下公司)宣布將關閉它。官方說法是他們的「商業優先順序已經改變」,並且「正全力專注於打造服務行銷人員、協助小型企業成長的工具」。 寫作者從來就不是他們的核心客群。

Mailchimp 於 2023 年底發布的關站公告。
Mailchimp 於 2023 年底發布的關站公告。
來源:EmailOctopus

就在 Tinyletter 於 2024 年 2 月 29 日關站前,我為訂閱者名單做了最後一次備份,但還沒想好接下來要怎麼處理它。

逃避

到這個時候,我開始排斥使用第三方服務的想法。同樣的故事很可能會再次上演。

我還是看了所有選項,但每一個都不合胃口:

  • 太貴了! 大多數服務都按聯絡人數量計費,而且假設你是在經營商業漏斗,而不是在寫信給人。
  • 太行銷導向了! 範本、拖拉式編輯器、A/B 測試、互動分數、追蹤像素。整套詞彙都不對。我不想經營什麼 campaigns;我只想寄 email!
  • 對駭客不友善。 沒有 markdown、沒有 CLI、沒有讓人想用的 API。一切都在為行銷團隊打造的網頁後台裡完成。
  • 不是開源。 如果下一個 Tinyletter 又關站,我希望能繼續下去,而不必再次遷移。
  • 預設就追蹤! 開信追蹤、點擊追蹤,每個頁尾都有像素。我不想知道誰開了什麼。我只想寫,你要看就看(或不看),就這樣。

遷移到 Fly.io

一直有人問我電子報什麼時候回歸,所以我在 fly.io 上拼湊了一個陽春版。那是一個小型的 Rust API、一個存有訂閱者的 CSV 檔案,以及一個透過網站訂閱的方式。想法是發送的部分之後再處理,至少先提供一個可以註冊的管道。

然後名單就這樣閒置在那裡。

事實證明,冷名單本身就是個問題。當你終於對一群很久沒收到你來信的人發送郵件時,郵件服務商會起疑,你可能會被標記為垃圾郵件。突然間,你自己的電子報反而會反過來對付你。

尋找發送服務

這無疑是最困難的部分。我研究了 ResendPostmarkSendGridMailgunAmazon SES 等等,還有很多。對一個小型的電子報來說,它們不是太貴、就是 API 很難用、不符合 GDPR(一般資料保護規則) 規範,不然就是過於複雜。

我差點就要放棄了,就在那時我找到了 Plunk。它是開源的,價格會隨著名單規模調整,而且 API 不會跟我作對。它幫我處理了我不想煩惱的 deliverability 工作(SES 整合bounce handling(退信處理)suppression list託管的取消訂閱頁面)。我現在是付費用戶。我跟他們沒有任何關聯,只是個真心滿意的用戶。

我甚至發了一個小小的貢獻,他們十分鐘內就合併了。這讓我感覺自己真的成了社群的一份子。

第一期真正的電子報寄給了一千多位久未收到我消息的聯絡人。我本來已經做好迎接大量 bounces 的準備,結果一切順利。bounce rate 大約 1%,只有極少數人取消訂閱,也沒有 deliverability 問題。哇!

我沒有搞什麼花招:沒有分批發送、沒有緩慢暖身、沒有巧妙的主旨行。我一次全部寄出,讓 Plunk(底層其實是 SES)透過 bounce handling 自動剔除明顯無效的地址。我唯一有做的一件事,就是在第一期的開頭加了一段簡短、坦誠的重新自我介紹——大概像是「嗨,你曾因為讀過我的一篇部落格文章而訂閱,抱歉這麼久沒消沒息」——我覺得這才是讓取消訂閱率保持低檔的主因。

以費用來說,寄給完整名單一次大約花我 $1。對於不定期發送的電子報來說,根本不算什麼。

Plunk 後台,顯示 campaign 總覽與 deliverability 報告。如你所見,我沒有追蹤誰開了我的信。
Plunk 後台,顯示 campaign 總覽與 deliverability 報告。如你所見,我沒有追蹤誰開了我的信。
來源:Plunk

這才像家!

我發現我可以把每期內容寫成資料夾裡的純 markdown 檔案,用版本控制管理,其他事情則用一個小小的 CLI 來處理。那才是我感到自在的地方。只有我、一杯熱可可、我的編輯器、終端機和 git。寫作與我之間不再隔著一個網頁後台。

整個系統都放在單一的 repo 裡:

newsletter/
├── issues/         # one .md per edition (1.md, 2.md, ...)
├── send/           # the CLI I run locally
└── subscribe/      # tiny HTTP service behind the website signup form

這個 CLI 叫做 send。它能做這些事:

$ send help

Usage: send <COMMAND>

Commands:
  new      Create a new issue file and open $EDITOR
  list     List local issues
  lint     Check links in an issue (or all issues)
  test     Send a test email to myself  
  publish  Publish the issue to all subscribed contacts
  status   Show contact-list and deliverability report
  prune    Delete unsubscribed contacts

send publish 2 會在實際發送前先顯示預覽、收件人數量,以及一個 y/N 確認提示。主旨行會自動組成為 corrode v0.N.0 # <topic>——採用 semver 風格,主版本號永遠停在 0,當作是對那些永遠到不了 1.0 的專案開的一個小玩笑。

send status 會顯示每個 campaign 的 deliverability,其中 bounce-rate 儲存格會根據 SES 的門檻值以不同顏色標示,還有每日的 bounces 和 unsubscribes,讓我能及早發現問題。

send lint 會在發布前用 lychee 檢查每期內容中的所有連結。我是 lychee 的維護者,所以在這裡自己試用自己的工具是理所當然的選擇,而且比起完全沒有連結檢查的舊 Tinyletter 網頁編輯器,這是個很棒的生活品質提升。

網站上的註冊表單會 POST 到小型的 subscribe 服務,該服務跑在我的伺服器上。它會驗證電子郵件,丟棄任何填了 honeypot 欄位的請求,然後以 subscribe-requested 事件 POST 到 Plunk。Plunk 會以 unsubscribed 狀態建立聯絡人,並透過其 Action 工作流程發送 transactional(交易型)確認信。只有當收件人點擊連結後,Plunk 才會將其狀態切換為 subscribed1。沒有回傳到我這端的 webhook,沒有 callback,頁面上也沒有 JavaScript。我只要 push 到 git,伺服器就會偵測到變更,建置並執行 server crate,新版本就上線了。執行中的服務幾乎不佔用任何 CPU 或記憶體。

關於 DNS,簡單說一下

Plunk 需要在 DNS 中設定三樣東西才能代我發信:一個 SPF 紀錄(聲明允許 SES 代表該網域發信)、一個 DKIM 金鑰(讓 SES 能為外寄郵件簽署),以及一個 return-path MX 紀錄(讓 bounces 能回到 Plunk 可讀取的地方)。這三項都位於子網域底下。別擔心,Plunk 會清楚告訴你如何設定,你只要把紀錄複製貼到你的 DNS 服務商後台即可。

有一件事千萬別忘了:不要在你網域的 apex 加入 Plunk 選用的 inbound MX。那會把郵件從目前處理你收件匣的服務商那裡搶走(以我來說是 mailbox.org),導致回信無法寄到你預期的地方。

一個小插曲

我忘了如果想讓回覆功能正常運作,From: 地址實際上必須是一個真實的信箱。第一期是用 [email protected] 寄出的,但那個信箱根本不存在。一位好心的讀者(嗨 Kevin!)回信打招呼,結果他的信被退回,於是他把退信通知轉寄給我告知此事。我在 mailbox.org 上建立了別名,從此回信就能正常進入我的收件匣了。

一份名單,而非兩份

趁這個機會,我也把舊的 endler.dev 電子報和 corrode.dev 的電子報合併成一份名單。兩份一直都是由我撰寫,同時維護兩套平行的設定其實從來就沒什麼道理。同一個人在鍵盤前、受眾大多重疊,卻要做兩倍的維護。

合併本身風平浪靜:我有一個從 Tinyletter 匯出的 CSV(原本的 endler.dev 名單),以及另一個來自我 fly.io 服務的 CSV(corrode.dev 上線後開始收集的 corrode.dev 名單)。格式相同。兩者都匯入 Plunk,deduplication(去重)完全不是問題。在第一期中,我明確說明了定位(一份電子報涵蓋我所有的寫作),所以沒有人需要猜測自己現在訂閱的是什麼。 2

往後,就只有一份電子報了。如果其中有任何內容不適合你,隨時可以取消訂閱,從此不再收到我的來信。完全不會放在心上。

想給你的建議

如果你一直在考慮要自己動手做:就去做吧。Self-hosting(自行架設)真的比以前容易多了。現在幾乎每個環節都有很棒的開源服務。一般來說,自己動手打造小東西是真正理解它們、並持續掌握重要環節的最佳方式之一。這本身就可以寫成另一篇部落格文章了,所以如果你想看,跟我說一聲。

如果你想偷看一下那個(有點拼湊的)repo,寄封信給我,我會傳連結給你。其實沒什麼特別有趣的,但如果你好奇它的運作方式,我很樂意分享。或者等我稍微整理一下、再正式開源,那大概還要再拖個幾年才會完成。

而最棒的是,你現在就可以填寫下方的表單、訂閱電子報,來試試我的設定!

  1. 點擊確認很重要,以符合 GDPR 規範,而這部分由 Plunk 幫我處理。

  2. 至於備份:Plunk 持有權威的訂閱者名單,我隨時可以將它匯出為 CSV。我想他們也有提供相關的 API,但我還沒試過。

原文由 Matthias Endler 發布

本文章由 muse-spark-1.2-contributor 進行翻譯