Skip to main content

Newsletter Library

The newsletter library provides configuration, validation, and utility functions for managing email newsletter subscriptions. It builds on top of the Mail Factory for email delivery.

File Structure

lib/newsletter/
config.ts # Configuration, types, Zod schemas, email config creation
utils.ts # Email sending, subscription validation, logging, templates

Configuration (config.ts)

Newsletter Constants

The NEWSLETTER_CONFIG object centralizes all newsletter-related constants:

export const NEWSLETTER_CONFIG = {
DEFAULT_PROVIDER: "resend",
DEFAULT_FROM: "onboarding@resend.dev",
DEFAULT_COMPANY_NAME: "Ever Works",

SOURCES: {
FOOTER: "footer",
POPUP: "popup",
SIGNUP: "signup",
} as const,

ERRORS: {
INVALID_EMAIL: "Please enter a valid email address",
ALREADY_SUBSCRIBED: "Email is already subscribed to the newsletter",
NOT_SUBSCRIBED: "Email is not subscribed to the newsletter",
SUBSCRIPTION_FAILED: "Failed to create subscription. Please try again.",
UNSUBSCRIPTION_FAILED: "Failed to unsubscribe. Please try again.",
EMAIL_SEND_FAILED: "Failed to send email. Please try again.",
STATS_FAILED: "Failed to get newsletter statistics",
} as const,

SUCCESS: {
SUBSCRIBED: "Successfully subscribed to newsletter",
UNSUBSCRIBED: "Successfully unsubscribed from newsletter",
} as const,
} as const;

Types

// Source of the subscription action
type NewsletterSource = "footer" | "popup" | "signup";

// Email provider configuration
interface EmailConfig {
provider: string;
defaultFrom: string;
domain: string;
apiKeys: { resend: string; novu: string };
novu?: { templateId?: string; backendUrl?: string };
}

// Action result for subscribe/unsubscribe operations
interface NewsletterActionResult {
success?: boolean;
error?: string;
email?: string;
}

// Newsletter statistics
interface NewsletterStats {
totalActive: number;
recentSubscriptions: number;
}

Validation Schemas

Two Zod schemas handle email validation for newsletter operations:

import { z } from "zod";

// Basic email validation
export const emailSchema = z.object({
email: z
.string()
.email(NEWSLETTER_CONFIG.ERRORS.INVALID_EMAIL)
.transform((email) => email.toLowerCase().trim()),
});

// Full subscription schema with source tracking
export const newsletterSubscriptionSchema = z.object({
email: z
.string()
.email(NEWSLETTER_CONFIG.ERRORS.INVALID_EMAIL)
.transform((email) => email.toLowerCase().trim()),
source: z
.enum(["footer", "popup", "signup"])
.default("footer"),
});

Config Creation

import { createEmailConfig, getCompanyName } from '@/lib/newsletter/config';

// Build email config from app settings and environment
const config = await createEmailConfig();
// => { provider: "resend", defaultFrom: "...", domain: "...", apiKeys: {...} }

// Get company name with fallback
const name = await getCompanyName();
// => "Ever Works" or value from content config

Utilities (utils.ts)

Sending Emails Safely

The sendEmailSafely function wraps email sending with comprehensive error handling:

import { sendEmailSafely, createEmailService } from '@/lib/newsletter/utils';

const { service, config } = await createEmailService();

const result = await sendEmailSafely(
service,
config,
{ subject: "Welcome!", html: "<p>Hi</p>", text: "Hi" },
"user@example.com",
"welcome"
);

if (result.success) {
// Email sent
} else {
console.log(result.error);
}

Subscription Validation

Check whether an email can be subscribed or unsubscribed:

import { canSubscribe, canUnsubscribe } from '@/lib/newsletter/utils';

// Check if email is not already active
const subCheck = await canSubscribe("user@example.com");
if (!subCheck.canSubscribe) {
console.log(subCheck.error); // "Email is already subscribed..."
}

// Check if email is currently active
const unsubCheck = await canUnsubscribe("user@example.com");
if (!unsubCheck.canUnsubscribe) {
console.log(unsubCheck.error); // "Email is not subscribed..."
}

The underlying validateSubscriptionStatus function queries the database:

const validation = await validateSubscriptionStatus(email, shouldBeActive);
// => { isValid: boolean, error?: string, subscription?: any }

Logging and Monitoring

import { logNewsletterActivity, trackNewsletterMetric } from '@/lib/newsletter/utils';

// Log activity for monitoring
logNewsletterActivity("subscribe", "user@example.com", "footer");
// Output: Newsletter Activity: { timestamp, action, email, source }

// Track metrics (wraps logNewsletterActivity)
trackNewsletterMetric("subscription", "user@example.com", "popup");

Template Utilities

import { getTemplateWithCompany } from '@/lib/newsletter/utils';

// Automatically injects the company name into a template function
const template = await getTemplateWithCompany(
(email, companyName) => ({
subject: `Welcome to ${companyName}`,
html: `<p>Thanks for subscribing, ${email}!</p>`,
text: `Thanks for subscribing, ${email}!`,
}),
"user@example.com"
);

Standardized Responses

import { createErrorResponse, createSuccessResponse } from '@/lib/newsletter/utils';

const error = createErrorResponse("Invalid email", "bad@", "subscribe");
// => { error: "Invalid email", email: "bad@", context: "subscribe" }

const success = createSuccessResponse("user@example.com", "subscribe");
// => { success: true, email: "user@example.com", context: "subscribe" }

Integration Flow

A typical newsletter subscription flow:

  1. Validate input using newsletterSubscriptionSchema
  2. Check eligibility using canSubscribe
  3. Create database record via the newsletter repository
  4. Send welcome email using sendEmailSafely
  5. Log activity using logNewsletterActivity
  6. Return result using createSuccessResponse or createErrorResponse
// Simplified server action example
async function subscribeToNewsletter(formData: FormData) {
const parsed = newsletterSubscriptionSchema.safeParse({
email: formData.get('email'),
source: formData.get('source'),
});
if (!parsed.success) {
return createErrorResponse(NEWSLETTER_CONFIG.ERRORS.INVALID_EMAIL);
}

const { email, source } = parsed.data;
const eligibility = await canSubscribe(email);
if (!eligibility.canSubscribe) {
return createErrorResponse(eligibility.error || "Cannot subscribe");
}

// Create subscription in database...
// Send welcome email...

logNewsletterActivity("subscribe", email, source);
return createSuccessResponse(email, "subscribe");
}
  • lib/newsletter/config.ts - Configuration, types, and validation schemas
  • lib/newsletter/utils.ts - Email, validation, logging, and template utilities
  • lib/mail/ - Email provider factory and service
  • lib/mail/templates/newsletter-*.ts - Newsletter email templates
  • lib/db/queries.ts - Database queries for subscription records