Signup form with double opt-in, delivering to Resend, Mailchimp or Buttondown.
A newsletter signup form you can drop on any page, with proper double opt-in: subscribers confirm by clicking a link in their inbox before they count. Confirmed subscribers are pushed to the provider you already use — Resend, Mailchimp or Buttondown — or simply kept in your own database if you'd rather export a CSV.
The spec below adds a subscriber content type, two custom API routes (subscribe + confirm), and a small form component. Pending subscribers are drafts; confirming publishes them and forwards them to the configured provider. Your provider API key stays server-side in an env var — the browser never sees it.
You are adding newsletter signup with double opt-in to a Prelo project. Read `prelo docs config` and `prelo docs api` if unsure.
1. **Content type.** In `cms.config.js`:
```js
subscriber: {
label: "Subscribers",
fields: {
email: { type: "text", required: true },
token: { type: "text" }, // confirm token, cleared after use
source: { type: "text" }, // page the signup came from
},
studio: { columns: ["email", "source"] },
},
```
Pending = draft, confirmed = published. Because the public API returns only published entries and this type should not be public at all, also ensure the frontend never links or renders `/api/subscriber` data.
2. **Provider config.** Support three providers behind env vars — implement only the adapter(s) the user asks for, defaulting to "local only" when no provider env is set:
- `NEWSLETTER_PROVIDER` = `resend` | `mailchimp` | `buttondown` (unset = local)
- Resend: `RESEND_API_KEY`, `RESEND_AUDIENCE_ID` → `POST https://api.resend.com/audiences/{id}/contacts`
- Mailchimp: `MAILCHIMP_API_KEY`, `MAILCHIMP_LIST_ID` (datacenter from the key suffix) → member upsert with `status: "subscribed"`
- Buttondown: `BUTTONDOWN_API_KEY` → `POST https://api.buttondown.email/v1/subscribers`
Put the adapter in one file (e.g. `newsletter-provider.js` next to `cms.config.js`) with a single `push(email)` export. Note in code comments that these are third-party services with their own terms.
3. **Routes.** In `cms.config.js` `routes:` (mounted under `/api/x/`):
- `"POST /newsletter"` — validate email format; if the email already exists as a subscriber, respond `200 {"ok":true}` (do not leak existence). Otherwise create a **draft** `subscriber` with a random 32-byte hex `token`, and email a confirm link `https://<site>/api/x/newsletter/confirm?token=…` using the project's SMTP config (the env vars `prelo doctor` reports). If SMTP is missing, respond `503` with a clear error — double opt-in cannot work without outbound email.
- `"GET /newsletter/confirm"` — look up the draft by token; if found, set it to published, clear the token, call the provider adapter (failures logged, not fatal), and redirect (302) to `/?subscribed=1` or a `/thanks` page if one exists.
Rate-limit `POST /newsletter` by IP (5/10 min) using an `x_newsletter_hits` table created in `hooks.boot`, same pattern as the contact-form plugin.
4. **Form component.** Add a `NewsletterSignup` component in the frontend: single email input + button, posts JSON to `/api/x/newsletter`. Success state: "Check your inbox to confirm." Inline error state. If the project uses page sections, register a `newsletterSignup` section (optional `heading` text field) so the client can place it from Studio.
5. **Verify.** `prelo check`; subscribe with a test address; confirm the draft appears in Studio; open the confirm link; confirm the entry flips to published and (if configured) reaches the provider.Subscribers are listed in Studio under Subscribers — drafts are unconfirmed, published are confirmed. Delete a row to unsubscribe locally; provider-side unsubscribes are handled by the provider's own links in your campaigns.
Collect and curate client quotes, rendered as a wall or a carousel.
Contact form with spam protection and email notifications, plus a submissions inbox in Studio.
Filterable gallery with lightbox, fed straight from the media library.