自行架設我的電子報
我在這個部落格上經營電子報好幾年了,但有很長一段時間一封信都沒寄過。這是我如何終於讓它重新上線的故事,以及一路上學到的事。
先快速說明一下,因為這點曾造成一些誤會:所謂的「自行架設」,指的是我沒有使用電子報平台。註冊的後端和用來發送每期內容的 CLI 都是我自己做的,而每期內容本身就只是 git 儲存庫裡的 markdown 檔案。我仍使用Plunk作為發送後端(所以 SES、bounces(退信)、suppression lists(抑制名單)和取消訂閱頁面就不是我要煩惱的問題)。Plunk 本身是開源的,我也可以自行架設,但 deliverability(寄達率)那一端有太多邊界狀況,我很樂意付費讓別人來處理。🙃
Tinyletter 時期

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

它就是能用。然後 Tinyletter 關站了。
一點歷史背景:Tinyletter 由 Philip Kaplan(菲利普·卡普蘭) 於 2010 年打造,據說是在2010 年 10 月 31 日那個週日一天之內寫出來的。
一年後它被 Mailchimp 收購,並悄悄成為想經營個人電子報、又不想煩惱什麼漏斗、分眾或 A/B 測試的寫作者們的實際首選。
然後在 2023 年底,Mailchimp(現在是 Intuit 旗下公司)宣布將關閉它。官方說法是他們的「商業優先順序已經改變」,並且「正全力專注於打造服務行銷人員、協助小型企業成長的工具」。 寫作者從來就不是他們的核心客群。

來源:EmailOctopus
就在 Tinyletter 於 2024 年 2 月 29 日關站前,我為訂閱者名單做了最後一次備份,但還沒想好接下來要怎麼處理它。
逃避
到這個時候,我開始排斥使用第三方服務的想法。同樣的故事很可能會再次上演。
我還是看了所有選項,但每一個都不合胃口:
- 太貴了! 大多數服務都按聯絡人數量計費,而且假設你是在經營商業漏斗,而不是在寫信給人。
- 太行銷導向了! 範本、拖拉式編輯器、A/B 測試、互動分數、追蹤像素。整套詞彙都不對。我不想經營什麼 campaigns;我只想寄 email!
- 對駭客不友善。 沒有 markdown、沒有 CLI、沒有讓人想用的 API。一切都在為行銷團隊打造的網頁後台裡完成。
- 不是開源。 如果下一個 Tinyletter 又關站,我希望能繼續下去,而不必再次遷移。
- 預設就追蹤! 開信追蹤、點擊追蹤,每個頁尾都有像素。我不想知道誰開了什麼。我只想寫,你要看就看(或不看),就這樣。
遷移到 Fly.io
一直有人問我電子報什麼時候回歸,所以我在 fly.io 上拼湊了一個陽春版。那是一個小型的 Rust API、一個存有訂閱者的 CSV 檔案,以及一個透過網站訂閱的方式。想法是發送的部分之後再處理,至少先提供一個可以註冊的管道。
然後名單就這樣閒置在那裡。
事實證明,冷名單本身就是個問題。當你終於對一群很久沒收到你來信的人發送郵件時,郵件服務商會起疑,你可能會被標記為垃圾郵件。突然間,你自己的電子報反而會反過來對付你。
尋找發送服務
這無疑是最困難的部分。我研究了 Resend、Postmark、SendGrid、Mailgun、Amazon SES 等等,還有很多。對一個小型的電子報來說,它們不是太貴、就是 API 很難用、不符合 GDPR(一般資料保護規則) 規範,不然就是過於複雜。
我差點就要放棄了,就在那時我找到了 Plunk。它是開源的,價格會隨著名單規模調整,而且 API 不會跟我作對。它幫我處理了我不想煩惱的 deliverability 工作(SES 整合、bounce handling(退信處理)、suppression list、託管的取消訂閱頁面)。我現在是付費用戶。我跟他們沒有任何關聯,只是個真心滿意的用戶。
我甚至發了一個小小的貢獻,他們十分鐘內就合併了。這讓我感覺自己真的成了社群的一份子。
第一期真正的電子報寄給了一千多位久未收到我消息的聯絡人。我本來已經做好迎接大量 bounces 的準備,結果一切順利。bounce rate 大約 1%,只有極少數人取消訂閱,也沒有 deliverability 問題。哇!
我沒有搞什麼花招:沒有分批發送、沒有緩慢暖身、沒有巧妙的主旨行。我一次全部寄出,讓 Plunk(底層其實是 SES)透過 bounce handling 自動剔除明顯無效的地址。我唯一有做的一件事,就是在第一期的開頭加了一段簡短、坦誠的重新自我介紹——大概像是「嗨,你曾因為讀過我的一篇部落格文章而訂閱,抱歉這麼久沒消沒息」——我覺得這才是讓取消訂閱率保持低檔的主因。
以費用來說,寄給完整名單一次大約花我 $1。對於不定期發送的電子報來說,根本不算什麼。

來源: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 contactssend 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,寄封信給我,我會傳連結給你。其實沒什麼特別有趣的,但如果你好奇它的運作方式,我很樂意分享。或者等我稍微整理一下、再正式開源,那大概還要再拖個幾年才會完成。
而最棒的是,你現在就可以填寫下方的表單、訂閱電子報,來試試我的設定!
隨機一篇部落格