innovedus_cms/docs/newsletter_integration_memo.md
Warren Chen b16ee811e3 feat: Add support for weekly newsletters with article selection and templates
- 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.
2026-05-14 18:17:35 +09:00

20 KiB
Raw Blame History

電子報介接備忘錄(租戶端 / Wagtail

最後更新2026-02-18

1. 目標與前提

本備忘錄用於整理租戶端Wagtail接下來的實作方向重點是先完成可運行流程再逐步補完細節。

2. 已確認決策(本次會議結論)

  1. Member Center 雖為共用平台,但目前只有一個租戶。
  2. 現階段「發確認信」與「訂閱/退訂頁面」先做在 Wagtail。
  3. Wagtail 與 Member Center 的溝通以 API 為主。
  4. SMTP relay 僅負責寄出一次重送、token 安全、簽章等由其他系統或工具負責,不屬 Wagtail 責任。
  5. 工作大項與順序固定為:
    1. 準備工作(電子報系統設定)
    2. 訂閱流程
    3. 退訂流程
    4. 電子報 app
    5. 發信流程(排程)

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 與系統歸屬標註規範(文件撰寫規則)

後續文件凡提到網址,必須明確標註「是哪個系統」:

  1. Wagtail租戶站台網址
  • 例:https://{tenant-site}/newsletter/confirm?token=...
  • 用途:使用者互動頁、站台前台/後台頁面。
  1. Member Center API 網址
  • 例:https://{member-center}/newsletter/subscribe
  • 用途:訂閱、確認、退訂 token、退訂等 API。
  1. Send Engine API 網址
  • 例:https://{send-engine}/api/send-jobs
  • 用途:建立/查詢/取消發信任務。
  1. SMTP Relay 連線端點
  • 用途Wagtail 寄送確認信。
  • 備註:僅作為寄送通道,不承擔重送與安全機制。

6. 非 Wagtail 責任邊界(本階段)

以下不納入目前 Wagtail 工作範圍:

  • SMTP 後續重送策略。
  • token 簽章與高階安全策略。
  • 跨系統風控與防濫用機制。

7. 下一步(實作啟動清單)

  1. 先完成電子報系統設定Member Center / Send Engine / SMTP / 模板)。
  2. 再完成訂閱流程 API 串接與確認頁。
  3. 再完成退訂頁與退訂 API 串接。
  4. 建立電子報 app 與 HTML 編輯器資料模型。
  5. 最後接排程任務與 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

  1. 訂閱
  • POST /newsletter/subscribe
  • JSON body必要
    • list_idGuid
    • emailstring
  • JSON body可選
    • preferencesobject
    • sourcestring
  • 回傳包含 confirm_token
  1. 訂閱確認(雙重驗證)
  • GET /newsletter/confirm?token=...
  • 注意:confirm 是 GET不是 POST
  1. 單一名單退訂
  • POST /newsletter/unsubscribe
  • JSON body必要
    • tokenstring
  1. 申請退訂 token
  • POST /newsletter/unsubscribe-token
  • JSON body必要
    • list_idGuid
    • emailstring
  • 回傳:
    • unsubscribe_tokenstring

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-Unsubscribe one-click header。
  • 呼叫發信代理 API。
  • 提供 unsubscribe endpointone-click + 人類點擊頁)。

9.2 Unsubscribe Token 設計

建議使用 JWT 或 HMAC token。

Token 內容建議:

  • subscriber_id
  • list_id
  • site_id
  • exp
  • nonceoptional

安全要求:

  • 不需登入即可退訂。
  • 必須可 idempotent。
  • Token 需有過期時間。
  • Token 必須簽章驗證。

9.3 Unsubscribe Endpoint

  1. One-click endpoint
  • POST /u/unsubscribe
  • Requesttoken
  • 流程:
    • 驗證 token
    • 呼叫會員平台 API
    • 回傳 200
  • 不可:
    • 要求登入
    • 要求二次確認
  1. 人類點擊頁面
  • 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_id
  • list_id
  • source: "one_click"
  • campaign_id

回傳:

  • success
  • already_unsubscribed

9.6 測試案例

  1. Token 驗證
  • 正常 token -> 成功退訂
  • 過期 token -> 400410
  • 已退訂 -> 200
  • 簽章錯誤 -> 400
  1. 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/unsubscribetoken 可由 query/body 提供)
    • GET /u/unsubscribe?token=...(人類點擊頁)
  • 已實作 token 驗證HMAC + exp)與 idempotent 行為(重複退訂維持 200
  • 已實作 server-side audit logOneClickUnsubscribeAudit)。
  • 已提供 one-click URL + Header 產生工具:
    • List-Unsubscribe
    • List-Unsubscribe-Post

9.9 Send Engine 發送介接(依 ../mass_mail_engine/docs/openapi.yaml 對齊)

已對齊的 API

  1. 建立 send job
  • POST /api/send-jobs
  • request 以 CreateSendJobRequest 為主(list_idsubjectbody_html/body_text/template
  • response 讀取:
    • send_job_id
    • status
  1. 查詢 send job
  • GET /api/send-jobs/{id}
  • 若建立後狀態非最終態,持續輪詢直到最終態或超過輪詢上限。

狀態對應Wagtail campaign

  • Send Engine completed -> campaign sent
  • Send Engine failed / cancelled -> campaign failed
  • 建立失敗 / 輪詢逾時 / 輪詢錯誤 -> campaign failed

Send Engine 最終態terminal

  • completed
  • failed
  • cancelled

備註:

  • 目前 dispatch 紀錄改為「每次送 job / 重試一次一筆」,不再是逐收件者一筆。
  • 若要做投遞到單一收件者的最終監控delivery/bounce/complaint仍建議接 Send Engine webhook 或事件回寫機制處理。

9.10 One-Click Token 模式(目前採用)

目前採用:MemberCenter token relay

流程:

  1. CMS 發送 payload 時同時提供:
    • template.list_unsubscribe_url_template(給 Send Engine 產生 List-Unsubscribe header 信內可點連結由 body_html/body_text 直接使用 {{unsubscribe_token}} 組 URL。
      例如:https://{cms}/u/unsubscribe?token={{unsubscribe_token}}
  2. Send Engine 於發送時向 Member Center 取得每位收件者的退訂 token並替換 {{unsubscribe_token}}
  3. 使用者點擊信內退訂(或 mailbox one-click進入 CMS /u/unsubscribe
  4. 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 為主。

目標:

  • 提供可編輯的 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

  1. 使用者在 Snippet 列表按「新增電子報」。
  2. 第一步顯示「選擇電子報類型」頁:
    • general 通用電子報
    • weekly_news 每週新聞電子報
  3. 選類型後進入對應編輯畫面(或同畫面動態顯示對應欄位)。
  4. 系統帶入該類型預設 template可改
  5. weekly_news 類型需額外完成:
    • 選文章(可多選、可排序)
    • 單則新聞樣式(預設值 + 可覆寫)
  6. 預覽按鈕顯示最終結果(文章內容 + 單則樣式 + 外層模板)。
  7. 發送時使用相同 renderer避免預覽與實際寄送不一致。

10.3 資料模型規劃(最小變更)

建議在 NewsletterCampaign 新增:

  • campaign_typeCharField
    • choices: general, weekly_news
    • default: general
  • item_template_htmlTextField, blank=True
    • 用於 weekly_news 單則新聞樣式覆寫
  • content_configJSONField, default=dict, blank=True
    • 先用於存 weekly_news 文章清單與排序資訊
    • 範例:
      • {"article_page_ids":[123,456,789]}

建議在 NewsletterTemplate 新增(可選,但推薦):

  • template_typeCharField
    • 用於限制模板類型對應(避免 weekly 套到不相容模板)
    • 若先求快可不加,先用通用模板 + placeholder 規範。

建議新增「類型預設設定」來源(二選一):

  1. 快速版:在程式常數定義每種類型預設值(包括預設 item template
  2. 長期版:加 NewsletterTypeSettingsWagtail setting/snippet供後台改預設值。

10.4 渲染與發送策略(核心)

新增 renderer 介面(概念):

  • build_campaign_body_html(campaign) -> str
  • build_campaign_body_text(campaign) -> str
  • build_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 實作步驟(建議)

  1. Model + migration
  • NewsletterCampaign 新增 campaign_type, item_template_html, content_config
  1. Admin form / panels
  • 先選類型流程create view+ 欄位顯示切換。
  • weekly_news 文章選擇與排序 UI第一版可先用簡單多選
  1. Renderer
  • 抽出 general / weekly_news 共用入口。
  • 串入 preview API 與 send scheduler。
  1. Preview
  • 擴充現有 preview compose 邏輯,使其依 campaign_type 分派。
  1. Send
  • _build_send_job_payload 改為依 renderer 組 body再送 Send Engine。
  1. 測試
  • general 回歸測試(不可壞)。
  • weekly_news 組版、預覽、發送一致性測試。

10.9 風險與控管

風險:

  • 類型欄位加入後,既有 campaign 資料相容性。
  • preview 與 send 使用不同邏輯導致內容不一致。
  • 文章多選排序 UI 複雜度偏高。

控管:

  • 既有資料 migration 一律補 campaign_type='general'
  • renderer 單一路徑preview/send 共用。
  • weekly_news 第一版先做可用,再迭代進階排序體驗。

10.10 後續新增定型電子報的擴充規範

每新增一種類型至少變更:

  1. campaign_type choices
  2. 該類型後台欄位配置
  3. 該類型 renderer
  4. 該類型 preview payload 驗證

此規範符合「新增定型電子報需要改程式」的產品決策,且可保持改動局部化。