import { randomUUID } from 'node:crypto' import { Resend } from 'resend' import { requireAdminAuth } from '../auth.js' import { state } from '../state.js' import { queueNewsletterIssuesWrite } from '../data.js' import { escapeHtml } from '../helpers.js' import { resendRetry } from '../email.js' const BASE = 'https://versebyversewithnate.us' const NEW_EPISODE_TOPIC_NAME = 'new episode' function truncate(str, max) { const clean = String(str || '').trim() if (clean.length <= max) return clean return `${clean.slice(0, max - 1).trimEnd()}…` } function stripSubjectPrefix(subject) { return String(subject || '').replace(/^New Episode:?\s*-?\s*/i, '').trim() } function slugify(str) { return String(str || '') .toLowerCase() .replace(/[^a-z0-9]+/g, '-') .replace(/^-+|-+$/g, '') .slice(0, 80) } function uniqueSlug(base) { const root = slugify(base) || 'issue' let candidate = root let n = 2 while (state.newsletterIssues.some(i => i.slug === candidate)) { candidate = `${root}-${n}` n += 1 } return candidate } function extractDescription(text, previewText) { if (previewText && previewText.trim()) return truncate(previewText, 200) const paragraphs = String(text || '') .split(/\n{2,}/) .map(p => p.replace(/\s+/g, ' ').trim()) .filter(Boolean) const candidate = paragraphs.find(p => p.length > 60 && !/^[◆✦✍✎▶]/.test(p) && p !== p.toUpperCase()) return truncate(candidate || paragraphs[paragraphs.length - 1] || '', 200) } // Strip the raw {{{RESEND_UNSUBSCRIBE_URL}}} placeholder link — it only resolves inside an actual sent email. function stripUnsubscribeLink(html) { return String(html || '').replace( /]*>\s*]*href="\{\{\{RESEND_UNSUBSCRIBE_URL\}\}\}"[\s\S]*?<\/a>\s*<\/p>/i, '', ) } function buildIssuePageHtml(issue) { const canonical = `${BASE}/newsletter/${encodeURIComponent(issue.slug)}` const ogImage = `${BASE}/images/banner.png` const title = escapeHtml(issue.title) const description = escapeHtml(issue.description) const metaBlock = ` ${title} — Verse by Verse with Nate ` const topBar = `
Verse by Verse with Nate ← All issues
` const ctaBlock = `

Stay in the Word

Get the next issue in your inbox

Subscribe by Email

← Back to the newsletter archive

` let html = stripUnsubscribeLink(issue.html) html = html.replace(/[\s\S]*?<\/title>/i, '') html = /<\/head>/i.test(html) ? html.replace(/<\/head>/i, metaBlock) : html.replace(/<head[^>]*>/i, m => `${m}${metaBlock.replace('</head>', '')}`) html = html.replace(/<body[^>]*>/i, m => `${m}${topBar}`) html = /<\/body>/i.test(html) ? html.replace(/<\/body>/i, ctaBlock) : `${html}${ctaBlock}` return html } export function register(app) { // ── Admin: browse Resend broadcasts, flagged by New Episode topic ──────── app.get('/api/admin/newsletter/resend-broadcasts', requireAdminAuth, async (_req, res) => { if (!process.env.RESEND_API_KEY) { res.status(500).json({ message: 'RESEND_API_KEY is not configured.' }); return } try { const resend = new Resend(process.env.RESEND_API_KEY) const [{ data: topicsData, error: topicsError }, { data: listData, error: listError }] = await Promise.all([ resendRetry(() => resend.topics.list()), resendRetry(() => resend.broadcasts.list()), ]) if (topicsError) throw new Error(topicsError.message) if (listError) throw new Error(listError.message) const newEpisodeTopic = (topicsData?.data ?? []).find(t => String(t.name || '').trim().toLowerCase() === NEW_EPISODE_TOPIC_NAME) const sentBroadcasts = (listData?.data ?? []).filter(b => b.status === 'sent') // Fetch a few at a time so a long broadcast history can't exceed Resend's requests-per-second limit. const details = [] for (let i = 0; i < sentBroadcasts.length; i += 4) { const chunk = await Promise.all(sentBroadcasts.slice(i, i + 4).map(b => resendRetry(() => resend.broadcasts.get(b.id)))) details.push(...chunk) } const broadcasts = details.map(({ data }, i) => { const summary = sentBroadcasts[i] if (!data) return null return { id: data.id, name: data.name, subject: data.subject, sentAt: data.sent_at ?? summary.sent_at, topicId: data.topic_id ?? null, matchesNewEpisodeTopic: !!newEpisodeTopic && data.topic_id === newEpisodeTopic.id, alreadyPublished: state.newsletterIssues.some(issue => issue.resendBroadcastId === data.id), } }).filter(Boolean).sort((a, b) => new Date(b.sentAt ?? 0).getTime() - new Date(a.sentAt ?? 0).getTime()) res.json({ broadcasts, newEpisodeTopicFound: !!newEpisodeTopic }) } catch (err) { console.error('[newsletter] failed to list Resend broadcasts:', err) res.status(502).json({ message: 'Could not reach Resend.' }) } }) // ── Admin: manage published issues ──────────────────────────────────────── app.get('/api/admin/newsletter/issues', requireAdminAuth, (_req, res) => { const issues = [...state.newsletterIssues] .sort((a, b) => new Date(b.sentAt ?? b.publishedAt).getTime() - new Date(a.sentAt ?? a.publishedAt).getTime()) .map(({ html, ...rest }) => rest) res.json({ issues }) }) app.post('/api/admin/newsletter/issues', requireAdminAuth, async (req, res) => { const { broadcastId, title: titleOverride, slug: slugOverride, public: isPublic } = req.body ?? {} if (!broadcastId || typeof broadcastId !== 'string') { res.status(400).json({ message: 'broadcastId is required.' }); return } if (state.newsletterIssues.some(i => i.resendBroadcastId === broadcastId)) { res.status(409).json({ message: 'This broadcast has already been published.' }); return } if (!process.env.RESEND_API_KEY) { res.status(500).json({ message: 'RESEND_API_KEY is not configured.' }); return } try { const resend = new Resend(process.env.RESEND_API_KEY) const { data, error } = await resend.broadcasts.get(broadcastId) if (error || !data) throw new Error(error?.message ?? 'Broadcast not found.') if (!data.html) { res.status(400).json({ message: 'This broadcast has no HTML content.' }); return } const title = typeof titleOverride === 'string' && titleOverride.trim() ? titleOverride.trim().slice(0, 200) : stripSubjectPrefix(data.subject) || data.name || 'Newsletter Issue' const slug = typeof slugOverride === 'string' && slugOverride.trim() ? uniqueSlug(slugOverride) : uniqueSlug(title) const issue = { id: randomUUID(), resendBroadcastId: data.id, slug, title, subject: data.subject ?? '', description: extractDescription(data.text, data.preview_text), html: data.html, sentAt: data.sent_at ?? null, publishedAt: new Date().toISOString(), public: isPublic !== false, } state.newsletterIssues.push(issue) queueNewsletterIssuesWrite() const { html, ...rest } = issue res.json({ ok: true, issue: rest }) } catch (err) { console.error('[newsletter] failed to publish issue:', err) res.status(502).json({ message: 'Could not fetch broadcast content from Resend.' }) } }) app.patch('/api/admin/newsletter/issues/:id', requireAdminAuth, (req, res) => { const issue = state.newsletterIssues.find(i => i.id === req.params.id) if (!issue) { res.status(404).json({ message: 'Issue not found.' }); return } if (typeof req.body?.title === 'string' && req.body.title.trim()) issue.title = req.body.title.trim().slice(0, 200) if (typeof req.body?.slug === 'string' && req.body.slug.trim() && req.body.slug.trim() !== issue.slug) { const candidate = slugify(req.body.slug) if (!candidate) { res.status(400).json({ message: 'Invalid slug.' }); return } if (state.newsletterIssues.some(i => i.id !== issue.id && i.slug === candidate)) { res.status(409).json({ message: 'That slug is already in use.' }); return } issue.slug = candidate } if (typeof req.body?.public === 'boolean') issue.public = req.body.public queueNewsletterIssuesWrite() const { html, ...rest } = issue res.json({ ok: true, issue: rest }) }) app.delete('/api/admin/newsletter/issues/:id', requireAdminAuth, (req, res) => { const idx = state.newsletterIssues.findIndex(i => i.id === req.params.id) if (idx === -1) { res.status(404).json({ message: 'Issue not found.' }); return } state.newsletterIssues.splice(idx, 1) queueNewsletterIssuesWrite() res.json({ ok: true }) }) // ── Public ───────────────────────────────────────────────────────────────── app.get('/api/newsletter/issues', (_req, res) => { const issues = state.newsletterIssues .filter(i => i.public !== false) .sort((a, b) => new Date(b.sentAt ?? b.publishedAt).getTime() - new Date(a.sentAt ?? a.publishedAt).getTime()) .map(({ html, ...rest }) => rest) res.json({ issues }) }) app.get('/newsletter/:slug', (req, res, next) => { const issue = state.newsletterIssues.find(i => i.slug === req.params.slug && i.public !== false) if (!issue) { next(); return } res.type('html').send(buildIssuePageHtml(issue)) }) }