diff --git a/.cursor/plans/agentmail_newsletter_integration_03119c86.plan.md b/.cursor/plans/agentmail_newsletter_integration_03119c86.plan.md index 336c87b..8f35539 100644 --- a/.cursor/plans/agentmail_newsletter_integration_03119c86.plan.md +++ b/.cursor/plans/agentmail_newsletter_integration_03119c86.plan.md @@ -86,22 +86,20 @@ Integrate AgentMail as an optional newsletter system with email subscriptions, a flowchart TD User[User] -->|Subscribe| NewsletterForm[NewsletterSignup Component] NewsletterForm -->|Mutation| ConvexDB[(Convex Database)] - + NewPost[New Blog Post Published] -->|Trigger| SendScript[Send Newsletter Script] SendScript -->|Query Subscribers| ConvexDB SendScript -->|Send Emails| AgentMail[AgentMail API] AgentMail -->|Deliver| Subscribers[Subscriber Inboxes] - + EmailIn[Email to Inbox] -->|Webhook| AgentMail AgentMail -->|Webhook| ConvexWebhook[Convex Webhook Handler] ConvexWebhook -->|Create Draft| ConvexDB - + ContactForm[Contact Form] -->|Send Email| AgentMail AgentMail -->|Notify| Developer[Developer Email] ``` - - ## Implementation Steps ### Step 1: Database Schema Updates @@ -146,8 +144,6 @@ emailDrafts: defineTable({ .index("by_createdAt", ["createdAt"]), ``` - - ### Step 2: Site Configuration **File:** `src/config/siteConfig.ts`Add newsletter configuration interface and default config: @@ -166,7 +162,7 @@ export interface NewsletterConfig { // Form field configuration requireName: boolean; // If true, show name field; if false, email only showNameOptional: boolean; // If true, show name as optional field - + // Home page signup home: { enabled: boolean; @@ -174,7 +170,7 @@ export interface NewsletterConfig { title: string; description: string; }; - + // Blog post signup (not pages) posts: { enabled: boolean; @@ -182,7 +178,7 @@ export interface NewsletterConfig { title: string; description: string; }; - + // Dedicated newsletter page page: { enabled: boolean; @@ -195,7 +191,7 @@ export interface NewsletterConfig { // Auto-send new blog posts autoSendNewPosts: boolean; // If true, automatically send when post published sendOnSync: boolean; // Send during npm run sync if new post detected - + // Email template fromName: string; fromEmail: string; // Uses inboxUsername@inboxDomain @@ -294,8 +290,6 @@ export const siteConfig: SiteConfig = { }; ``` - - ### Step 3: Newsletter Signup Component **File:** `src/components/NewsletterSignup.tsx`Create reusable signup form component matching existing UI patterns: @@ -416,22 +410,31 @@ export default function NewsletterSignup({ } ``` - - ### Step 4: Convex Newsletter Functions **File:** `convex/newsletter.ts`Create backend functions following Convex best practices: ```typescript -import { query, mutation, internalMutation, internalAction, action } from "./_generated/server"; +import { + query, + mutation, + internalMutation, + internalAction, + action, +} from "./_generated/server"; import { v } from "convex/values"; import { internal } from "./_generated/api"; import crypto from "crypto"; // Generate unsubscribe token function generateUnsubscribeToken(email: string): string { - const secret = process.env.UNSUBSCRIBE_SECRET || "default-secret-change-in-production"; - return crypto.createHash("sha256").update(email + secret).digest("hex").substring(0, 32); + const secret = + process.env.UNSUBSCRIBE_SECRET || "default-secret-change-in-production"; + return crypto + .createHash("sha256") + .update(email + secret) + .digest("hex") + .substring(0, 32); } // Subscribe to newsletter @@ -508,7 +511,7 @@ export const unsubscribe = mutation({ }), handler: async (ctx, args) => { const email = args.email.toLowerCase().trim(); - + const subscriber = await ctx.db .query("newsletterSubscribers") .withIndex("by_email", (q) => q.eq("email", email)) @@ -584,7 +587,7 @@ export const getActiveSubscribers = internalQuery({ .query("newsletterSubscribers") .withIndex("by_subscribed", (q) => q.eq("subscribed", true)) .collect(); - + return subscribers.map((sub) => ({ email: sub.email, name: sub.name, @@ -636,9 +639,12 @@ export const sendPostNewsletter = internalAction({ }), handler: async (ctx, args) => { // Check if already sent - const alreadySent = await ctx.runQuery(internal.newsletter.hasPostBeenSent, { - postSlug: args.postSlug, - }); + const alreadySent = await ctx.runQuery( + internal.newsletter.hasPostBeenSent, + { + postSlug: args.postSlug, + }, + ); if (alreadySent) { return { @@ -662,7 +668,9 @@ export const sendPostNewsletter = internalAction({ } // Get subscribers - const subscribers = await ctx.runQuery(internal.newsletter.getActiveSubscribers); + const subscribers = await ctx.runQuery( + internal.newsletter.getActiveSubscribers, + ); if (subscribers.length === 0) { return { @@ -681,7 +689,7 @@ export const sendPostNewsletter = internalAction({ // Build email content const siteUrl = process.env.SITE_URL || "https://markdown.fast"; const postUrl = `${siteUrl}/${post.slug}`; - + let emailContent = `
${post.description}
`; @@ -743,9 +751,9 @@ export const getPostBySlugInternal = internalQuery({ .query("posts") .withIndex("by_slug", (q) => q.eq("slug", args.slug)) .first(); - + if (!post) return null; - + return { slug: post.slug, title: post.title, @@ -770,8 +778,6 @@ export const getPostBySlugInternal = internalQuery({ }); ``` - - ### Step 5: Newsletter Send Script **File:** `scripts/send-newsletter.ts`Create script to send newsletters (similar to `sync-posts.ts`): @@ -816,8 +822,6 @@ const postSlug = args[0]; sendNewsletterForPost(postSlug); ``` - - ### Step 6: Auto-Send Integration with Sync Script **File:** `scripts/sync-posts.ts`Modify sync script to detect new posts and auto-send if enabled: @@ -834,9 +838,7 @@ async function checkAndSendNewPosts( if (!autoSend) return; // Get all synced post slugs - const syncedSlugs = syncedPosts - .filter((p) => p.published) - .map((p) => p.slug); + const syncedSlugs = syncedPosts.filter((p) => p.published).map((p) => p.slug); // Check which posts haven't been sent yet for (const slug of syncedSlugs) { @@ -862,8 +864,6 @@ async function checkAndSendNewPosts( await checkAndSendNewPosts(parsedPosts, client); ``` - - ### Step 7: Newsletter Page **File:** `content/pages/newsletter.md`Create dedicated newsletter signup page: @@ -900,8 +900,6 @@ if (page && page.slug === siteConfig.newsletter.signup.page.slug) { } ``` - - ### Step 8: Unsubscribe Page **File:** `src/pages/Unsubscribe.tsx`Create unsubscribe page: @@ -973,8 +971,6 @@ export default function Unsubscribe() {