Skip to content

@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 email
  • GET /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:

  1. Fetches the entry from D1
  2. Queries all active subscribers
  3. Generates a per-subscriber signed unsubscribe URL
  4. 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).