Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

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

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

Expand Down
8 changes: 5 additions & 3 deletions docs/MILESTONES.md
Original file line number Diff line number Diff line change
Expand Up @@ -240,11 +240,13 @@ Privacy-friendly editor analytics. Closes the "what's working?" gap left after v

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

### v2.0 — Engagement and growth (pending)
### v2.0 — Engagement and growth (in progress)

**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.
Shipping in four PRs grouped by theme. **a** + **b** done, **c** + **d** to follow.

**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.
**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.

**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.

**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.

Expand Down
13 changes: 13 additions & 0 deletions drizzle/0008_nostalgic_namorita.sql
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
CREATE TABLE `subscribers` (
`id` text PRIMARY KEY NOT NULL,
`email` text NOT NULL,
`locale` text NOT NULL,
`token` text NOT NULL,
`confirmed_at` text,
`unsubscribed_at` text,
`source` text NOT NULL,
`created_at` text NOT NULL
);
--> statement-breakpoint
CREATE UNIQUE INDEX `subscribers_email_unique` ON `subscribers` (`email`);--> statement-breakpoint
CREATE UNIQUE INDEX `subscribers_token_unique` ON `subscribers` (`token`);
Loading
Loading