- Introduced `campaign_type` field in `NewsletterCampaign` model to differentiate between general and weekly newsletters. - Added `weekly_articles` ManyToMany field to associate articles with weekly newsletters. - Implemented `item_template_html` for customizing the appearance of articles in weekly newsletters. - Updated forms and views to handle new fields and validation logic for weekly newsletters. - Created a new view for selecting newsletter types before creating a campaign. - Enhanced the newsletter editor UI to show/hide fields based on the selected campaign type. - Added JavaScript functionality for paginating article selections in the editor. - Updated CSS for new UI components related to newsletter campaigns. - Created a migration to add new fields to the `NewsletterCampaign` model. - Added a template for the newsletter type selection page.
20 KiB
20 KiB
電子報介接備忘錄(租戶端 / Wagtail)
最後更新:2026-02-18
1. 目標與前提
本備忘錄用於整理租戶端(Wagtail)接下來的實作方向,重點是先完成可運行流程,再逐步補完細節。
2. 已確認決策(本次會議結論)
- Member Center 雖為共用平台,但目前只有一個租戶。
- 現階段「發確認信」與「訂閱/退訂頁面」先做在 Wagtail。
- Wagtail 與 Member Center 的溝通以 API 為主。
- SMTP relay 僅負責寄出一次,重送、token 安全、簽章等由其他系統或工具負責,不屬 Wagtail 責任。
- 工作大項與順序固定為:
- 準備工作(電子報系統設定)
- 訂閱流程
- 退訂流程
- 電子報 app
- 發信流程(排程)
3. 工作大項與執行順序(含範圍)
3.1 準備工作(電子報系統設定,第一優先)
範圍:
- 建立 Member Center 連線設定(base URL、tenant_id、list_id、API timeout 等)。
- 建立 Send Engine 連線設定(base URL、OAuth scope、API timeout 等)。
- 建立一般發信設定(SMTP relay、寄件者名稱/信箱、reply-to、預設編碼等)。
- 建立模板設定:
- 訂閱確認信 template
- 訂閱確認(成功)頁面 template
- 取消訂閱頁面 template
重點:
- 設定集中管理,避免散落在程式碼或多處後台欄位。
- 區分「系統連線設定」與「內容模板設定」,便於權限與維運。
完成標準(DoD):
- 可在單一設定入口完成上述設定並通過基本驗證。
- 模板可被後續訂閱/退訂流程直接引用。
3.2 訂閱流程(第二優先)
範圍:
- 在站台頁面區塊(暫定 Footer)提供 email 訂閱入口。
- 後端接收 email,呼叫 Member Center 訂閱 API。
- 由 Wagtail 送出訂閱確認信(透過 SMTP relay)。
- 使用者點信內連結回到 Wagtail 確認頁。
- 確認頁自動呼叫 Member Center 確認 API。
重點:
- 流程中 token 需一路帶入與驗證。
- 確認信內容採 HTML(可附純文字 fallback)。
完成標準(DoD):
- 可從站台成功發起訂閱。
- 可收到確認信並完成確認。
- 確認成功/失敗頁有明確訊息與追蹤記錄。
3.3 退訂流程(第三優先)
範圍:
- 電子報底部提供退訂連結,導向 Wagtail 退訂頁。
- 進入退訂頁時,先向 Member Center 申請退訂 token。
- 使用者按確認退訂後,呼叫 Member Center 退訂 API 完成流程。
重點:
- token 不做長期保存。
- 頁面需可處理 token 失效/不存在等錯誤狀態。
完成標準(DoD):
- 使用者能從信件連結完成退訂。
- 退訂結果頁可正確呈現成功/失敗狀態。
3.4 電子報 app(第四優先)
範圍:
- 在 Wagtail 後台提供電子報內容管理能力。
- 提供 HTML 編輯器建立/編修內容。
- 支援可替換參數(例如 token、email)。
重點:
- 編輯器中的連結參數以佔位符表示,發送前由 backend 替換。
- 若有 Member Center API 相關連結需在內容中可配置,也採相同參數機制。
完成標準(DoD):
- 編輯器可存草稿與更新內容。
- 內容中參數可在送出前正確替換。
3.5 發信流程(排程,第五優先)
範圍:
- 可建立發送任務與排程時間。
- 排程前可持續編輯內容與調整時間。
- 到時觸發時:先取 Member Center auth,再將內容包固定 header/footer,送到 Send Engine。
重點:
- 「內容定稿」與「實際送出」拆開。
- 發送前的模板組裝由 Wagtail backend 負責。
完成標準(DoD):
- 排程到點可成功建立 Send Engine send job。
- 失敗可記錄原因並可人工重試。
4. 參數替換規則(先行約定)
建議佔位符格式(待實作時可再定版):
{{token}}{{email}}{{list_id}}{{tenant_id}}{{confirm_url}}{{unsubscribe_url}}
替換時機:
- 發信前 backend 最後一步統一替換。
注意事項:
- URL 參數需做 URL encode。
- 缺少必要參數時,中止發送並記錄可追蹤錯誤。
5. URL 與系統歸屬標註規範(文件撰寫規則)
後續文件凡提到網址,必須明確標註「是哪個系統」:
- Wagtail(租戶站台)網址
- 例:
https://{tenant-site}/newsletter/confirm?token=... - 用途:使用者互動頁、站台前台/後台頁面。
- Member Center API 網址
- 例:
https://{member-center}/newsletter/subscribe - 用途:訂閱、確認、退訂 token、退訂等 API。
- Send Engine API 網址
- 例:
https://{send-engine}/api/send-jobs - 用途:建立/查詢/取消發信任務。
- SMTP Relay 連線端點
- 用途:Wagtail 寄送確認信。
- 備註:僅作為寄送通道,不承擔重送與安全機制。
6. 非 Wagtail 責任邊界(本階段)
以下不納入目前 Wagtail 工作範圍:
- SMTP 後續重送策略。
- token 簽章與高階安全策略。
- 跨系統風控與防濫用機制。
7. 下一步(實作啟動清單)
- 先完成電子報系統設定(Member Center / Send Engine / SMTP / 模板)。
- 再完成訂閱流程 API 串接與確認頁。
- 再完成退訂頁與退訂 API 串接。
- 建立電子報 app 與 HTML 編輯器資料模型。
- 最後接排程任務與 Send Engine 發信。
8. Member Center API 實際規格(以 member_center 程式碼 / OpenAPI 為準)
資料來源(2026-02-17 比對):
../member_center/src/MemberCenter.Api/Controllers/NewsletterController.cs../member_center/docs/openapi.yaml
8.1 Base URL 與路徑
- OpenAPI
servers.url為/api,部署時實際呼叫通常為:https://{member-center}/api/newsletter/...
- 若部署已在 gateway 做 path rewrite,也可為:
https://{member-center}/newsletter/...
- 租戶端必須以實際部署路徑設定
member_center_base_url。
8.2 Endpoint / Method / Request
- 訂閱
POST /newsletter/subscribe- JSON body(必要):
list_id(Guid)email(string)
- JSON body(可選):
preferences(object)source(string)
- 回傳包含
confirm_token
- 訂閱確認(雙重驗證)
GET /newsletter/confirm?token=...- 注意:confirm 是 GET,不是 POST
- 單一名單退訂
POST /newsletter/unsubscribe- JSON body(必要):
token(string)
- 申請退訂 token
POST /newsletter/unsubscribe-token- JSON body(必要):
list_id(Guid)email(string)
- 回傳:
unsubscribe_token(string)
8.3 與租戶端目前實作對齊結果
subscribe:已使用POST。confirm:已改為GET + query token(修正 HTTP 405 問題)。unsubscribe:已使用POST,且 body 僅送token。unsubscribe-token:已使用POST,且 body 送list_id + email。
9. List-Unsubscribe One-Click 規範(發信流程階段導入)
說明:
- 本節為「電子報 app + 排程發信」階段的目標規範。
- 已完成的訂閱/退訂流程維持現況,不回頭重做;待發信流程上線時再整合 one-click。
9.1 目標
- 提供電子報編輯與排程。
- 產生
List-Unsubscribeone-click header。 - 呼叫發信代理 API。
- 提供 unsubscribe endpoint(one-click + 人類點擊頁)。
9.2 Unsubscribe Token 設計
建議使用 JWT 或 HMAC token。
Token 內容建議:
subscriber_idlist_idsite_idexpnonce(optional)
安全要求:
- 不需登入即可退訂。
- 必須可 idempotent。
- Token 需有過期時間。
- Token 必須簽章驗證。
9.3 Unsubscribe Endpoint
- One-click endpoint
POST /u/unsubscribe- Request:
token - 流程:
- 驗證 token
- 呼叫會員平台 API
- 回傳
200
- 不可:
- 要求登入
- 要求二次確認
- 人類點擊頁面
GET /u/unsubscribe?token=xxx- 流程:
- 驗證 token
- 顯示退訂成功頁面
9.4 發信 Header 規則
Newsletter 必須包含:
List-Unsubscribe: <https://domain/u/unsubscribe?token=xxx>List-Unsubscribe-Post: List-Unsubscribe=One-Click
註記:
- Transactional email 不應包含 List-Unsubscribe。
9.5 與會員平台整合(one-click 目標介面)
呼叫 API:
POST /api/subscriptions/unsubscribe
Request:
subscriber_idlist_idsource: "one_click"campaign_id
回傳:
successalready_unsubscribed
9.6 測試案例
- Token 驗證
- 正常 token -> 成功退訂
- 過期 token ->
400或410 - 已退訂 ->
200 - 簽章錯誤 ->
400
- Header 驗證
- Newsletter 發送時必定包含
List-Unsubscribe - Transactional email 不包含
List-Unsubscribe - Header 格式正確
9.7 安全與 UX
安全:
- Unsubscribe endpoint 不受 CSRF 限制
- Token 驗證必須 server-side
- 記錄 audit log
- 不顯示
subscriber_id在 URL 明文
UX:
- 退訂成功頁面簡潔
- 提供重新訂閱入口
- 不強迫填問卷
9.8 目前實作狀態(Wagtail)
- 已提供 one-click endpoint:
POST /u/unsubscribe(token 可由 query/body 提供)GET /u/unsubscribe?token=...(人類點擊頁)
- 已實作 token 驗證(HMAC +
exp)與 idempotent 行為(重複退訂維持 200)。 - 已實作 server-side audit log(
OneClickUnsubscribeAudit)。 - 已提供 one-click URL + Header 產生工具:
List-UnsubscribeList-Unsubscribe-Post
9.9 Send Engine 發送介接(依 ../mass_mail_engine/docs/openapi.yaml 對齊)
已對齊的 API:
- 建立 send job
POST /api/send-jobs- request 以
CreateSendJobRequest為主(list_id、subject、body_html/body_text/template) - response 讀取:
send_job_idstatus
- 查詢 send job
GET /api/send-jobs/{id}- 若建立後狀態非最終態,持續輪詢直到最終態或超過輪詢上限。
狀態對應(Wagtail campaign):
- Send Engine
completed-> campaignsent - Send Engine
failed/cancelled-> campaignfailed - 建立失敗 / 輪詢逾時 / 輪詢錯誤 -> campaign
failed
Send Engine 最終態(terminal):
completedfailedcancelled
備註:
- 目前 dispatch 紀錄改為「每次送 job / 重試一次一筆」,不再是逐收件者一筆。
- 若要做投遞到單一收件者的最終監控(delivery/bounce/complaint),仍建議接 Send Engine webhook 或事件回寫機制處理。
9.10 One-Click Token 模式(目前採用)
目前採用:MemberCenter token relay
流程:
- CMS 發送 payload 時同時提供:
template.list_unsubscribe_url_template(給 Send Engine 產生List-Unsubscribeheader) 信內可點連結由body_html/body_text直接使用{{unsubscribe_token}}組 URL。
例如:https://{cms}/u/unsubscribe?token={{unsubscribe_token}}
- Send Engine 於發送時向 Member Center 取得每位收件者的退訂 token,並替換
{{unsubscribe_token}}。 - 使用者點擊信內退訂(或 mailbox one-click)後,進入 CMS
/u/unsubscribe。 - CMS 不自行驗簽 token,直接以 S2S 呼叫 Member Center
unsubscribe(token)完成退訂。
備註:
- 舊的 CMS 自簽 HMAC one-click token 保留工具函式作為備援,但主流程已切到 relay 模式。
10. 下一階段演進備忘(先記錄,待試用回饋後再排)
說明:
- 本節為「規劃中」項目,先記錄方向,不立即實作。
- 優先等待:電子報排版產出 + 實際 user 試用回饋。
10.1 編輯器能力演進
現況:
- 目前使用 Wagtail Draftail 作為 HTML 編輯器,已可插入圖片(圖庫選擇 / 上傳)。
待回饋後評估:
- 若編輯需求提升(table、欄位版型、拖拉區塊、進階 email 元件),再評估導入更完整的 newsletter editor。
- 原則:先確認真實編輯痛點,再導入新編輯器,避免過早複雜化。
10.2 電子報樣式(CSS)策略
建議方向:
- 採「固定樣式基底 + 內容編輯」模式。
- CSS 不建議只放
<head><style>,實務上需考慮 email client 相容性,通常會在發送前做 CSS inlining(將重要樣式轉為 inline style)。 - 可保留少量 head style(例如 media query),但核心排版建議以 inline 為主。
10.3 外框模板(Header / Footer / Shell)管理
目標:
- 提供可編輯的 newsletter shell(例如
layout.html),內含 head、header、footer 與內容插槽。
建議實作概念:
- 內容編輯區僅負責 body 內文。
- 發送時由 backend 組裝:
shell + content + one-click headers/links。 - 預覽也走同一組裝流程,避免「預覽長得跟實際寄出不一致」。
10.4 退訂連結與個人化參數責任分工
共識方向:
- 退訂連結需與收件者身分綁定(每位收件者唯一 token / URL)。
- 內容模板需預留替換位(例如
{{unsubscribe_url}})。
責任分工建議:
- CMS:產內容、產 placeholder、組裝信件與 one-click header。
- Send Engine:執行發送、接收投遞回報(bounce/complaint 等)。
- SES(或下游 provider):最終投遞與回執來源。
10.5 文章區塊直接引入
需求方向:
- 支援從既有文章中挑選區塊或全文片段匯入電子報。
建議:
- 第一階段可先做「選文章 -> 帶入標題/摘要/首圖/連結」。
- 第二階段再考慮「可編輯快照」或「保持動態同步」策略。
10.6 預覽能力
需求方向:
- 內文編輯預覽。
- 樣式(shell)編輯預覽。
- 能注入假資料(假收件者、假 token、假文章)看最終結果。
建議:
- 做「最終寄出 HTML 預覽」功能,預覽管線與正式發送使用同一套 renderer。
- 後續可加多裝置寬度與常見 email client 快速檢視模式(若使用者回饋有需要)。
10. 電子報「類型化編輯」設計分析與實作計劃(2026-05-13)
需求摘要(已確認):
- 建立電子報時,先選「電子報類型」再進入編輯。
- 每種電子報類型有預設 template,但允許使用者改模板。
- 既有類型:
通用電子報(自由編輯內容)。 - 新增類型:
每週新聞電子報(選文章組版)。 每週新聞除模板與文章選擇外,還要有「單則新聞 HTML 樣式」:- 系統提供預設值。
- 每封電子報可覆寫(通常不改,必要時可改)。
- 預覽功能必須保留,且要反映「套模板後最終結果」。
- 後續新增定型電子報,接受「需要改程式」。
10.1 可行性結論
可行,且建議採「類型策略(type strategy)」實作:
- 資料層只新增最少欄位承載「類型 + 類型專屬設定」。
- 組版/預覽/發送統一走同一組 renderer 介面,依類型分派。
- 後續加新類型時,不改既有類型流程,僅新增一個 renderer + 後台欄位配置。
10.2 目標操作流程(UX)
- 使用者在 Snippet 列表按「新增電子報」。
- 第一步顯示「選擇電子報類型」頁:
general通用電子報weekly_news每週新聞電子報
- 選類型後進入對應編輯畫面(或同畫面動態顯示對應欄位)。
- 系統帶入該類型預設 template(可改)。
weekly_news類型需額外完成:- 選文章(可多選、可排序)
- 單則新聞樣式(預設值 + 可覆寫)
- 預覽按鈕顯示最終結果(文章內容 + 單則樣式 + 外層模板)。
- 發送時使用相同 renderer,避免預覽與實際寄送不一致。
10.3 資料模型規劃(最小變更)
建議在 NewsletterCampaign 新增:
campaign_type(CharField)- choices:
general,weekly_news - default:
general
- choices:
item_template_html(TextField, blank=True)- 用於
weekly_news單則新聞樣式覆寫
- 用於
content_config(JSONField, default=dict, blank=True)- 先用於存
weekly_news文章清單與排序資訊 - 範例:
{"article_page_ids":[123,456,789]}
- 先用於存
建議在 NewsletterTemplate 新增(可選,但推薦):
template_type(CharField)- 用於限制模板類型對應(避免 weekly 套到不相容模板)
- 若先求快可不加,先用通用模板 + placeholder 規範。
建議新增「類型預設設定」來源(二選一):
- 快速版:在程式常數定義每種類型預設值(包括預設 item template)。
- 長期版:加
NewsletterTypeSettings(Wagtail setting/snippet)供後台改預設值。
10.4 渲染與發送策略(核心)
新增 renderer 介面(概念):
build_campaign_body_html(campaign) -> strbuild_campaign_body_text(campaign) -> strbuild_preview_html(campaign_or_payload) -> str
類型分派:
general:- 直接沿用既有
html_template/text_template流程。
- 直接沿用既有
weekly_news:- 讀
content_config.article_page_ids - 載入文章(標題、連結、摘要、封面等)
- 以
item_template_html逐筆渲染並串接成email_body - 再套外層
newsletter_template.template_html
- 讀
注意:
- 預覽 API 與發送 job 必須共用同一 renderer。
- 不把套完外層模板的結果存回 DB(維持既有原則:預覽/發送當下組裝)。
10.5 後台表單與互動規劃
A. 建立流程(先選類型)
- 做法 1(推薦):客製
CreateView,先顯示campaign_type,提交後導到 edit。 - 做法 2:進 create 頁先顯示全欄位,但用 JS 依
campaign_type切換可見區塊。
B. general 欄位
- 顯示:
subject_template,html_template,text_template,newsletter_template - 隱藏:
weekly_news文章選擇區與 item template(或只讀)
C. weekly_news 欄位
- 顯示:
newsletter_template- 文章選擇器(多選 + 排序)
item_template_html(有預設值,可覆寫)- (可選)
subject_template(若空可由 template 預設 subject 帶入)
- 隱藏:
- 自由編輯
html_template(或改成唯讀顯示最終組版來源)
- 自由編輯
10.6 預覽功能規劃
新增/調整 preview endpoint:
- 接收
campaign_type與對應 payload。 weekly_news預覽時直接用目前畫面文章清單 + item template 組版,不必先存檔。- 回傳最終 HTML(已套外層模板、已替換示例變數)。
10.7 每週新聞(Weekly)預設單則樣式建議
預設 item_template_html 建議包含:
{{article_title}}{{article_url}}{{article_cover_url}}{{article_intro}}{{article_date}}
建議 fallback 規則:
- 無封面圖:隱藏
<img>區塊或改用預設圖。 - 無摘要:顯示標題 + 連結即可。
10.8 實作步驟(建議)
- Model + migration
NewsletterCampaign新增campaign_type,item_template_html,content_config。
- Admin form / panels
- 先選類型流程(create view)+ 欄位顯示切換。
weekly_news文章選擇與排序 UI(第一版可先用簡單多選)。
- Renderer
- 抽出
general/weekly_news共用入口。 - 串入 preview API 與 send scheduler。
- Preview
- 擴充現有 preview compose 邏輯,使其依
campaign_type分派。
- Send
_build_send_job_payload改為依 renderer 組 body,再送 Send Engine。
- 測試
general回歸測試(不可壞)。weekly_news組版、預覽、發送一致性測試。
10.9 風險與控管
風險:
- 類型欄位加入後,既有 campaign 資料相容性。
- preview 與 send 使用不同邏輯導致內容不一致。
- 文章多選排序 UI 複雜度偏高。
控管:
- 既有資料 migration 一律補
campaign_type='general'。 - renderer 單一路徑,preview/send 共用。
weekly_news第一版先做可用,再迭代進階排序體驗。
10.10 後續新增定型電子報的擴充規範
每新增一種類型至少變更:
campaign_typechoices- 該類型後台欄位配置
- 該類型 renderer
- 該類型 preview payload 驗證
此規範符合「新增定型電子報需要改程式」的產品決策,且可保持改動局部化。