feat(v2.0b): newsletter — fully optional (closes part of #35)#44
Merged
Conversation
Second of four v2.0 PRs. Newsletter is wired so it works at three
levels of operator commitment:
Level 0 (default): no provider configured
→ public subscribe form returns 503 OR (if `allowSingleOptIn` is
checked, default true) creates rows immediately confirmed.
Operator can manually export/email later.
Level 1: Resend API key + sender configured
→ public subscribe sends a real double-opt-in email.
Subscribers active only after clicking the confirmation link.
Level 2: cron-trigger pointing at /api/newsletter/send-digest
→ automated weekly digest (operator wires the cron in their
wrangler.toml; we ship the endpoint).
Schema (Drizzle migration 0008)
-------------------------------
- subscribers: id, email UNIQUE, locale, token UNIQUE (24-char
nanoid for confirm + unsubscribe links), confirmedAt nullable,
unsubscribedAt nullable, source, createdAt.
Provider
--------
- ContentProvider gains 8 newsletter methods: list/count/getByEmail/
getByToken/create (with autoConfirm flag for single-opt-in mode)/
confirm/unsubscribe/delete. All idempotent where it matters.
Newsletter helper module ($lib/server/newsletter/index.ts)
----------------------------------------------------------
- readNewsletterConfig(settings): pulls newsletter.* keys from
site_settings.
- isProviderConfigured(cfg): true when both resendKey + senderAddress
are present.
- sendEmail(cfg, args): Resend POST. Returns { ok: true, id? } or
{ ok: false, reason }. Best-effort — never throws.
- buildConfirmEmail({...}): per-locale subject + html + text.
- buildDigestEmail({...}): per-locale weekly digest with embedded
unsubscribe link.
Public endpoints
----------------
- POST /api/newsletter/subscribe (honeypot, idempotent on existing
emails to prevent enumeration, audit-logged)
- GET /api/newsletter/confirm?token=... (idempotent → 302 to
localized home with ?newsletter=confirmed)
- GET /api/newsletter/unsubscribe?token=... (one-click, no
interstitial — GDPR/CAN-SPAM)
Admin endpoint
--------------
- POST /api/newsletter/send-digest?days=7&dryRun=1 (admin-only,
iterates active subscribers, groups by locale, sends via Resend.
Returns { ok, sent, failed } or { ok, dryRun: true, subscribers,
articleCounts }. 503 when no provider configured.)
CMS surface
-----------
- /cms/subscribers (admin+): list with status badges (pending /
active / unsubscribed), provider-status banner, manual "Send
digest" + dry-run button when provider is on. Sidebar gets a
"Subscribers" entry under Admin.
- /cms/settings: new "Newsletter (optional)" card with three
fields: resendKey (masked-ish, font-mono), senderAddress, and
a checkbox for the allow-single-opt-in fallback behavior.
Audit
-----
- New AuditAction members: newsletter.{subscribe,confirm,
unsubscribe,delete,digest_sent}.
i18n: 22 new keys (EN + TH) across cms_subscribers_*,
cms_settings_newsletter_*.
Migration 0008 already applied to live D1.
Roadmap state
-------------
v2.0a Forms ✅ (PR #43)
v2.0b Newsletter ✅ (this PR)
v2.0c Comments 🚧
v2.0d Webhooks + Public REST API 🚧
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
thunpisit
added a commit
to codustry/khaopad-example
that referenced
this pull request
May 2, 2026
…y#44) (#12) Cherry-picks upstream PR codustry#44. Migration 0008 already applied to live D1. i18n: 22 new keys field-merged into messages/en.json + th.json without overwriting example-specific copy. Co-authored-by: Claude Opus 4.7 <noreply@anthropic.com>
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Summary
Second of four PRs for v2.0. Newsletter shipping with everything optional so operators can adopt at their own pace.
Three operator levels
What ships
Roadmap state after this merges
Test plan
🤖 Generated with Claude Code