Skip to content

Commit 08731f9

Browse files
thunpisitclaude
andauthored
feat(v2.0b): newsletter — fully optional (matches upstream PR codustry#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>
1 parent 070b957 commit 08731f9

21 files changed

Lines changed: 3226 additions & 5 deletions

File tree

README.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -284,7 +284,7 @@ Khao Pad started as a CMS. Through v1.5 it became a complete content layer (writ
284284
| **v1.7** | Pages, navigation, IA | ✅ Shipped | Media folders, reusable blocks, cookie consent, static pages, navigation manager, seed:legal |
285285
| **v1.8** | Analytics & insight | ✅ Shipped | Privacy-friendly D1 page-views, top articles, search-term insights, per-article sparkline, optional CWA |
286286
| **v1.9** | Performance & trust | ✅ Shipped | Responsive `srcset` via /cdn-cgi/image, edge cache-control hook, branded 404/500, /api/health endpoint |
287-
| **v2.0** | Engagement & growth | 🚧 Pending | Forms, newsletter, comments, webhooks, public read-only API |
287+
| **v2.0** | Engagement & growth | 🚧 In progress | ✅ a Forms · ✅ b Newsletter (optional) · 🚧 c Comments · 🚧 d Webhooks + Public REST API |
288288

289289
**Backlog** (not committed): OAuth providers, block-based editor, AI-assisted authoring, multi-site / workspaces, A/B testing, member-only / paid content.
290290

docs/MILESTONES.md

Lines changed: 5 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -240,11 +240,13 @@ Privacy-friendly editor analytics. Closes the "what's working?" gap left after v
240240

241241
**i18n** — 7 new `error_*` keys (EN + TH).
242242

243-
### v2.0 — Engagement and growth (pending)
243+
### v2.0 — Engagement and growth (in progress)
244244

245-
**Forms** — A new `forms` table (id, name, fields-as-JSON) and `form_submissions` table. CMS editor for fields (text, email, textarea, checkbox). Public submission endpoint with built-in honeypot + rate limit. Submissions land in the CMS for review; optional webhook on submit.
245+
Shipping in four PRs grouped by theme. **a** + **b** done, **c** + **d** to follow.
246246

247-
**Newsletter** — Subscriber list (`subscribers` table: email, locale, confirmedAt, unsubscribedAt, source). Opt-in via a form on the public site with double-confirm email. CMS digest job pulls the last week's published articles and sends a templated email via Resend / Cloudflare Email Routing. Compliance: clear unsubscribe link in every email, audit log of subscribe/unsubscribe events.
247+
**v2.0a — Forms (shipped)** — Drizzle migration 0007: `forms` (key UNIQUE, fields-as-JSON, `enabled` flag, per-locale success messages) and `form_submissions` (data JSON, ip_hash 16-char truncated SHA-256 — never raw IP, status enum new/read/spam/archived, note). Public `POST /api/forms/[key]` accepts multipart/url-encoded with honeypot field `_hp` and per-IP rate limit (3/minute). 410 when form disabled, 429 on rate-limit. CMS at `/cms/forms` with editor (add/reorder/delete fields of kind text/email/textarea/checkbox, per-field name + label + required toggle) and an embedded submissions inbox with mark-as / delete actions. New `form.{create,update,delete,submit}` audit actions.
248+
249+
**v2.0b — Newsletter (shipped, fully optional)** — Drizzle migration 0008: `subscribers` (email UNIQUE, locale, token UNIQUE, confirmedAt, unsubscribedAt, source). Optional everywhere: when no email provider is configured, public signups go single-opt-in (subscribers immediately confirmed) — clearly documented. When the operator sets a Resend API key + sender address in `/cms/settings → Newsletter`, public signups become double-opt-in: a confirmation email goes out via Resend, subscriber is "active" only after they click the link. Public endpoints: `POST /api/newsletter/subscribe` (form-data with email + locale, honeypot `_hp`, per-IP rate-limit-ready via the v2.0a hashIp helper), `GET /api/newsletter/confirm?token=...` (idempotent click target → 302 to localized home with `?newsletter=confirmed`), `GET /api/newsletter/unsubscribe?token=...` (one-click, no interstitial — GDPR/CAN-SPAM compliance). Admin endpoint `POST /api/newsletter/send-digest?days=7&dryRun=1` iterates active subscribers, groups by locale, picks the last week's published articles per locale, sends one email per subscriber via Resend. CMS `/cms/subscribers` (admin+ only) lists subscribers with status badge (pending/active/unsubscribed), exposes manual "Send digest now" + dry-run button when a provider is configured, shows a clear "no provider configured" banner with a link to settings when not. New `newsletter.{subscribe,confirm,unsubscribe,delete,digest_sent}` audit actions. Cron-trigger wiring deferred to operator's wrangler.toml.
248250

249251
**Comments** — Per-article comments with name + email, queued for moderation by default. CMS moderation queue at `/cms/comments`. Akismet-style spam filter is out of scope; rate limit + honeypot is the v2.0 floor. Optional: anchor in `<article>` body.
250252

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,13 @@
1+
CREATE TABLE `subscribers` (
2+
`id` text PRIMARY KEY NOT NULL,
3+
`email` text NOT NULL,
4+
`locale` text NOT NULL,
5+
`token` text NOT NULL,
6+
`confirmed_at` text,
7+
`unsubscribed_at` text,
8+
`source` text NOT NULL,
9+
`created_at` text NOT NULL
10+
);
11+
--> statement-breakpoint
12+
CREATE UNIQUE INDEX `subscribers_email_unique` ON `subscribers` (`email`);--> statement-breakpoint
13+
CREATE UNIQUE INDEX `subscribers_token_unique` ON `subscribers` (`token`);

0 commit comments

Comments
 (0)