Hosting My Own Newsletter

Matthias Endler

自己架設電子報

原文由 Matthias Endler 發布,訂閱此部落格

這個部落格的電子報開了好幾年,但有很長一段時間我一封信都沒寄。這篇文章要講的是,我最後怎麼讓它重新上線,以及一路學到的事。

先快速說明一下,因為這點曾引起一些誤會:我說的「自己架設」,指的是我沒有用現成的電子報平台。註冊的後端和用來發刊的 CLI 都是我自己做的,每一期內容就只是放在 git repo 裡的 markdown 檔。我還是用 Plunk 當作發信後端(所以 SES、退信、抑制名單、取消訂閱頁面這些就不用我煩惱)。Plunk 本身是開源的,我當然也可以自己架,但寄達率那一塊狀況太多了,我很樂意付錢讓別人幫我處理。🙃

Tinyletter 時代

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

好幾年來,我的作法就是在網站上放一個小小的表單,連到 Tinyletter——一個專為寫作者設計的小型電子報服務。我喜歡它的簡單。我完全不用去想什麼寄達率、退信率、抑制名單、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 測試、互動分數、追蹤像素。整套詞彙都不對。我不是要跑什麼行銷活動,我只是想寄電子郵件
  • 對駭客不友善。沒有 markdown、沒有 CLI、沒有讓人想用的 API。所有操作都要在為行銷團隊設計的網頁後台裡完成。
  • 不是開源。如果下一個 Tinyletter 又關了,我希望能繼續做下去,而不必再次大費周章搬家。
  • 預設就追蹤!開啟追蹤、點擊追蹤,每封信的頁尾都塞像素。我不想知道誰開了什麼。我想寫,你要看就看(不看也沒關係),就這樣。

搬到 Fly.io

一直有人問我電子報什麼時候會回來,所以我在 fly.io 上勉強拼了一個東西。就是一個小小的 Rust API、一個放訂閱者的 CSV 檔,還有一個讓大家在網站上訂閱的方法。想法是發信的事之後再說,至少先讓大家有地方可以訂閱。

然後那份名單就一直擺在那裡。

後來才發現,放太久沒寄的名單本身就是個問題。當你終於要寄信給一群很久沒收到你消息的人,郵件服務商會起疑,你很可能被當成垃圾郵件。突然間,你自己的電子報反而會反過來害到你。

尋找發信服務

這是目前為止最困難的部分。我研究過 ResendPostmarkSendGridMailgunAmazon SES,還有更多。結果不是對小型電子報來說太貴,就是 API 很難用、不符合 GDPR 規範,或是複雜到不行。

我差點就要放棄了,才找到 Plunk。它是開源的,價格會隨著名單大小調整,API 也不會跟我作對。它幫我處理那些我不想煩惱的寄達率工作(SES 整合退信處理抑制名單代管的取消訂閱頁面)。我現在是付費用戶。我跟他們沒有任何合作關係,只是真的用得很滿意。

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

第一封真正的電子報就這樣寄給了一千多位久未收到我消息的聯絡人。我本來已經做好會迎來一波退信的心理準備,結果一切順利。退信率大約 1%,取消訂閱的人也很少,沒有遇到寄達率的問題。太驚喜了!

我沒搞什麼花招:沒有分批寄、沒有慢慢暖身、也沒有精心設計的主旨。我一次全部寄出,讓 Plunk(底層其實是 SES)透過退信處理自動剔除那些明顯無效的地址。我唯一做的,就是在第一期的開頭放了一段簡短、誠懇的自我介紹——大概像是「嗨,你曾經因為讀過我的一篇文章而訂閱,抱歉這麼久沒消沒息」——我覺得這才是讓取消訂閱率維持很低的關鍵。

以費用來說,寄給完整名單一次大概只要 $1。對一封不定期發送的電子報而言,根本不算什麼。

Plunk 的後台,顯示活動總覽和寄達率報告。如你所見,我沒有追蹤誰開了信。
Plunk 的後台,顯示活動總覽和寄達率報告。如你所見,我沒有追蹤誰開了信。
來源: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 會顯示每一波活動的寄達情況,退信率的欄位會根據 SES 的門檻以不同顏色標示,還有每日的退信和取消訂閱數,讓我能及早發現問題。

send lint 會在發布前用 lychee 檢查每一期裡的所有連結。我本身就是 lychee 的維護者,所以在這裡自己用自家工具是再自然不過的選擇,比起完全沒有連結檢查的舊 Tinyletter 網頁編輯器,這算是很棒的體驗提升。

網站上的訂閱表單會用 POST 送到跑在我伺服器上的小型 subscribe 服務。它會驗證電子郵件地址,丟棄任何填了 honeypot 欄位的請求,然後用 subscribe-requested 事件 POST 到 Plunk。Plunk 會先以未訂閱狀態建立聯絡人,並透過它的 Action workflow 寄出交易型的確認信。只有當收件人點擊連結後,Plunk 才會將狀態翻轉為已訂閱1。不需要 webhook 回傳到我這邊,沒有 callback,頁面上也沒有 JavaScript。我只要 push 到 git,伺服器就會偵測到變更、建置並執行 server crate,新版本就上線了。執行中的服務幾乎不占任何 CPU 或記憶體。

簡單聊一下 DNS

Plunk 需要在 DNS 裡設定三樣東西才能代我寄信:SPF 紀錄(聲明 SES 可以用這個網域寄信)、DKIM 金鑰(讓 SES 可以為寄出的信件簽章),以及 return-path 的 MX 紀錄(讓退信能回到 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 上的服務(corrode.dev 上線後開始收集的名單)。格式一樣。兩份都匯進 Plunk,去除重複完全不是問題。我在第一期裡也把定位講得很清楚(一份涵蓋我所有寫作的電子報),所以沒人需要猜自己現在到底訂了什麼。2

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

想給你的建議

如果你也一直在考慮要不要自己來:就去做吧。自架真的比以前容易多了。現在幾乎每個環節都有很棒的開源服務。一般來說,自己動手做些小東西是真正理解它們、並保有那些重要部分的最好方法之一。這本身就可以另寫一篇部落格文章了,如果你想看,跟我說一聲。

如果你想看一下那個(有點拼湊的)repo,寫信給我,我就把連結傳給你。其實沒什麼特別的,但如果你好奇它是怎麼運作的,我很樂意分享。或者就等我哪天整理好、正式開源——不過那可能還要再拖個幾年我才會去做。

而最棒的是,你現在就可以填下面的表單、訂閱電子報,來實際試試我的這套架設!

  1. 點擊確認是為了符合 GDPR 規範,這部分就交給 Plunk 幫我處理。

  2. 至於備份:Plunk 保存著最權威的訂閱者名單,我隨時可以匯出成 CSV。我想他們應該也有提供相關的 API,不過我還沒試過。

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

留言