@indiepub/email
Newsletter delivery via Resend. Handles welcome emails for new subscribers, per-subscriber unsubscribe tokens, and newsletter delivery with batching.
Configuration
Enable subscriptions in your Astro config:
indiepub({ subscriptions: { enabled: true, fromEmail: 'Your Name <newsletter@yourdomain.com>', },})Required env vars: RESEND_API_KEY, RESEND_FROM_EMAIL
When enabled, two routes are injected:
POST /subscribe— creates a subscriber and sends a welcome emailGET /unsubscribe?token=...— verifies the HMAC token and marks the subscriber as unsubscribed
Sending newsletters
From the admin panel, open any published entry and click Send Newsletter. This triggers POST /admin/newsletter which:
- Fetches the entry from D1
- Queries all
activesubscribers - Generates a per-subscriber signed unsubscribe URL
- Calls
sendNewsletter()from this package
You can also call the API directly from any server-side code:
import { sendNewsletter } from '@indiepub/email';
await sendNewsletter( entry, recipients, // Array<{ email, unsubscribeUrl }> siteConfig, // IndiePubConfig apiKey, // RESEND_API_KEY);Unsubscribe tokens
Tokens are HMAC-SHA256 signatures over email:subscriberId using RESEND_API_KEY as the secret (via the Web Crypto API). They are included in every newsletter as a signed URL, so no token storage is needed.
import { generateUnsubscribeToken, verifyUnsubscribeToken, buildUnsubscribeUrl } from '@indiepub/email';
const token = await generateUnsubscribeToken(email, subscriberId, secret);const url = buildUnsubscribeUrl(siteUrl, email, subscriberId, token);const valid = await verifyUnsubscribeToken(email, subscriberId, token, secret);Batching
Resend’s batch API accepts up to 100 emails per call. sendNewsletterBatch() automatically chunks large subscriber lists:
import { sendNewsletterBatch } from '@indiepub/email';
const results = await sendNewsletterBatch(messages, apiKey);// results: NewsletterSendResult[]Subscribe form
To add a subscribe form to your theme, POST to /subscribe with application/x-www-form-urlencoded or application/json:
<form method="POST" action="/subscribe"> <input type="email" name="email" required /> <button type="submit">Subscribe</button></form>Or via JSON for fetch-based forms:
await fetch('/subscribe', { method: 'POST', headers: { 'Content-Type': 'application/json', Accept: 'application/json' }, body: JSON.stringify({ email: 'reader@example.com' }),});The response is either a redirect (form) or JSON { success: true } (fetch).