Salvation page: redesigned notification email, visitor confirmation email, clear on-screen confirmation and email notice, phone fixes; v2.39.1
Publish Container / build (push) Successful in 15m35s

Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
This commit is contained in:
nmemmertandClaude Sonnet 5.5 committed 2026-10-13 16:12:07 -04:00
1 parent 27656b84f2
commit b3e887aef2
3 files changed
+271 -28

No files matched your search

+1 -1
View File
@@ -1,7 +1,7 @@
{ {
"name": "siteforge", "name": "siteforge",
"private": true, "private": true,
"version": "2.39.0", "version": "2.39.1",
"type": "module", "type": "module",
"scripts": { "scripts": {
"dev": "vite", "dev": "vite",
+44 -4
View File
@@ -590,6 +590,24 @@
.callout-skip { color: var(--parchment-dark); font-size: 1rem; font-style: italic; text-decoration: underline; opacity: 0.85; } .callout-skip { color: var(--parchment-dark); font-size: 1rem; font-style: italic; text-decoration: underline; opacity: 0.85; }
.callout-skip:hover { opacity: 1; } .callout-skip:hover { opacity: 1; }
@media (max-width: 600px) { .record-callout { padding: 1.5rem 1.1rem; } .record-callout p { font-size: 1.1rem; } } @media (max-width: 600px) { .record-callout { padding: 1.5rem 1.1rem; } .record-callout p { font-size: 1.1rem; } }
.phase-label { white-space: nowrap; }
.filter-bar { scrollbar-width: none; }
.filter-bar::-webkit-scrollbar { display: none; }
@media (max-width: 720px) {
.hero-actions { flex-direction: column; align-items: stretch; max-width: 18rem; margin-left: auto; margin-right: auto; }
.hero-btn { text-align: center; }
.phase-header { flex-wrap: wrap; row-gap: 0.4rem; }
.phase-title { flex-basis: 100%; }
}
.sent-badge { width: 3.4rem; height: 3.4rem; border-radius: 50%; display: grid; place-items: center; font-size: 1.8rem; color: #fff; background: linear-gradient(135deg, #2e7d52, #1e6040); margin-bottom: 0.8rem; box-shadow: 0 2px 10px rgba(30,96,64,0.4); }
.sent-reach { font-size: 1.2rem !important; }
.sent-welcome { margin-top: 1.4rem; padding: 1.1rem 1.3rem; border-left: 4px solid var(--gold); background: rgba(184,134,11,0.1); }
.sent-welcome-head { font-family: 'Cinzel', serif; font-weight: 700; font-size: 1.05rem; letter-spacing: 0.05em; color: var(--ink); }
.sent-welcome .note { margin-top: 0.6rem; }
.sent-welcome a { color: var(--crimson); font-weight: 600; }
.field-hint { margin-top: 0.4rem; font-size: 0.98rem; line-height: 1.55; color: var(--ink-light); font-style: italic; }
</style> </style>
</head> </head>
<body> <body>
@@ -744,8 +762,9 @@
<input class="hp" type="text" name="_honey" tabindex="-1" autocomplete="off" aria-hidden="true"> <input class="hp" type="text" name="_honey" tabindex="-1" autocomplete="off" aria-hidden="true">
<label for="r-name">First name</label> <label for="r-name">First name</label>
<input type="text" id="r-name" name="name" maxlength="100" autocomplete="given-name"> <input type="text" id="r-name" name="name" maxlength="100" autocomplete="given-name">
<label for="r-email">Email (if you would like someone to reply)</label> <label for="r-email">Email (optional)</label>
<input type="email" id="r-email" name="email" maxlength="200" autocomplete="email"> <input type="email" id="r-email" name="email" maxlength="200" autocomplete="email" aria-describedby="r-email-hint">
<p class="field-hint" id="r-email-hint">If you add your email, we will send you a short confirmation message right away, and someone from the team will reach out to you personally. It is a one-time email, and we will not add you to any mailing list. If you leave it blank, we will still receive your message, but we will not be able to reply.</p>
<label for="r-msg">Anything you would like to share</label> <label for="r-msg">Anything you would like to share</label>
<textarea id="r-msg" name="message" rows="4" maxlength="2000"></textarea> <textarea id="r-msg" name="message" rows="4" maxlength="2000"></textarea>
<p class="form-error" id="r-error" role="alert"></p> <p class="form-error" id="r-error" role="alert"></p>
@@ -756,8 +775,14 @@
</form> </form>
</div> </div>
<div class="respond-done" id="respond-done" role="status" tabindex="-1"> <div class="respond-done" id="respond-done" role="status" tabindex="-1">
<h3 class="plea-title">Pardoned. Welcome to the family of God.</h3> <div class="sent-badge" aria-hidden="true">✓</div>
<p class="note">“Therefore, there is now no condemnation for those who are in Christ Jesus.” (Romans 8:1) You are no longer a defendant. You are a child of the Judge. Thank you for letting us know. We will be glad to hear from you again, and <a href="/start-here">Start Here</a> is a good path through the podcast and studies, or you can <a href="/contact?source=salvation">ask a question</a>.</p> <h3 class="plea-title">Your message has been sent.</h3>
<p class="note sent-reach" id="sent-reach"></p>
<div class="sent-welcome">
<p class="sent-welcome-head">Pardoned. Welcome to the family of God.</p>
<p class="note">“Therefore, there is now no condemnation for those who are in Christ Jesus.” (Romans 8:1) You are no longer a defendant. You are a child of the Judge.</p>
<p class="note">While you wait, begin with the Gospel of John, talk to God every day, and look for a Bible-teaching church. <a href="/start-here">Start Here</a> is a good path through the podcast and studies, and you can <a href="/contact?source=salvation">ask a question</a> any time.</p>
</div>
</div> </div>
</div> </div>
@@ -1621,8 +1646,23 @@ buildTimeline();
}); });
const data = await res.json().catch(function () { return {}; }); const data = await res.json().catch(function () { return {}; });
if (!res.ok) throw new Error(data.message || 'We could not send this right now.'); if (!res.ok) throw new Error(data.message || 'We could not send this right now.');
const reach = document.getElementById('sent-reach');
const given = (form.email.value || '').trim();
if (given) {
reach.textContent = '';
reach.appendChild(document.createTextNode(data.confirmationSent ? 'We just sent a confirmation email to ' : 'Someone from the team will reach out to you soon at '));
const strong = document.createElement('strong');
strong.textContent = given;
reach.appendChild(strong);
reach.appendChild(document.createTextNode(data.confirmationSent
? ', and someone from the team will reach out to you personally soon. If you do not see our emails, please check your spam folder.'
: '. If you do not see a reply in a few days, please check your spam folder.'));
} else {
reach.textContent = 'We are rejoicing with you. Because you did not leave an email address, we are not able to reach out to you, but you are always welcome to write to us.';
}
intro.style.display = 'none'; intro.style.display = 'none';
done.classList.add('open'); done.classList.add('open');
done.scrollIntoView({ block: 'center' });
done.focus(); done.focus();
} catch (ex) { } catch (ex) {
err.textContent = (ex && ex.message ? ex.message : 'We could not send this right now.') + ' Your decision is between you and God either way, and you are welcome to write to us from the contact page.'; err.textContent = (ex && ex.message ? ex.message : 'We could not send this right now.') + ' Your decision is between you and God either way, and you are welcome to write to us from the contact page.';
+226 -23
View File
@@ -1,10 +1,25 @@
import { Resend } from 'resend' import { Resend } from 'resend'
import { escapeHtml } from '../helpers.js' import { escapeHtml } from '../helpers.js'
import { getResendFromAddress, getResendInboxAddress, sendResendEmailWithRetry } from '../email.js' import { getResendFromAddress, getResendInboxAddress, getResendReplyToAddress, getCanonicalBaseUrl, sendResendEmailWithRetry } from '../email.js'
// A visitor who finished the /salvation page can tell us they prayed to receive Christ. // A visitor who finished the /salvation page can tell us they prayed to receive Christ.
// Nothing is stored on the server: the response is emailed once to the site inbox and that is all. // Nothing is stored on the server: the response is emailed once to the site inbox and that is all.
const hits = new Map() const hits = new Map()
// One confirmation per address per hour, so the form can't be used to mail someone repeatedly.
const confirmedAt = new Map()
const CONFIRM_COOLDOWN_MS = 60 * 60 * 1000
function mayConfirm(address) {
const key = address.toLowerCase()
const now = Date.now()
const last = confirmedAt.get(key)
if (typeof last === 'number' && now - last < CONFIRM_COOLDOWN_MS) return false
confirmedAt.set(key, now)
if (confirmedAt.size > 2000) {
for (const [k, v] of confirmedAt) { if (now - v > CONFIRM_COOLDOWN_MS) confirmedAt.delete(k) }
}
return true
}
function rateLimit(req, res, next) { function rateLimit(req, res, next) {
const ip = req.ip ?? 'unknown' const ip = req.ip ?? 'unknown'
@@ -28,6 +43,188 @@ function clean(value, max) {
return typeof value === 'string' ? value.trim().replace(/\s+/g, ' ').slice(0, max) : '' return typeof value === 'string' ? value.trim().replace(/\s+/g, ' ').slice(0, max) : ''
} }
// The notification the site owner receives. Styled to match the other admin emails
// (dark header, gold accent, cream cards) and written to be acted on, not just read.
function buildSalvationEmail({ name, email, message, when }) {
const who = name || 'Someone'
const subject = name ? `A new decision for Christ: ${name}` : 'A new decision for Christ on your salvation page'
const nameLine = name || 'Name not given'
const safeName = escapeHtml(nameLine)
const safeEmail = escapeHtml(email)
const safeWhen = escapeHtml(when)
const followUp = email
? [
'Reply to this email within a day or two. A short, warm note is plenty.',
'Congratulate them, and ask them to tell you their story.',
'Point them to the Gospel of John, a Bible-teaching church, and baptism.',
'Invite them to Start Here at versebyversewithnate.us/start-here.',
'Pray for them by name.',
]
: ['They did not leave an email address, so there is no way to reply.', 'Pray for them. God knows exactly who they are.']
const text = [
`A VERDICT HAS COME IN`,
``,
`${who} pleaded guilty and accepted Christ's pardon on the "The Gospel, in a Courtroom" page and says they prayed to receive Jesus as their Savior.`,
``,
`"There is joy in the presence of the angels of God over one sinner who repents." (Luke 15:10)`,
``,
`DETAILS`,
`Name: ${nameLine}`,
`Email: ${email || 'Not given'}`,
`When: ${when}`,
``,
`THEIR MESSAGE`,
message || '(They did not leave a message.)',
``,
`SUGGESTED FOLLOW-UP`,
...followUp.map((t, i) => `${i + 1}. ${t}`),
``,
`This email is the only record of this response. Nothing is stored on the website, so keep it somewhere safe.`,
email ? `Reply directly to this email to reach ${who}.` : '',
].filter(l => l !== '').join('\n')
const row = (label, value, last = false) =>
`<tr><td style="width:96px;padding:11px 0;font-family:Arial,sans-serif;font-size:12px;font-weight:700;letter-spacing:0.08em;text-transform:uppercase;color:#8a6d35;${last ? '' : 'border-bottom:1px solid #efe4cc;'}">${label}</td>` +
`<td style="padding:11px 0;font-family:Arial,sans-serif;font-size:15px;color:#201a10;${last ? '' : 'border-bottom:1px solid #efe4cc;'}">${value}</td></tr>`
const steps = followUp
.map(t => `<li style="margin:0 0 8px;font-family:Arial,sans-serif;font-size:15px;line-height:1.55;color:#201a10;">${escapeHtml(t)}</li>`)
.join('')
const messageBlock = message
? `<tr><td style="padding:0 28px 22px;">
<div style="font-family:Arial,sans-serif;font-size:12px;font-weight:700;letter-spacing:0.14em;text-transform:uppercase;color:#8a6d35;margin:0 0 8px;">Their message</div>
<div style="background:#fbf7ef;border-left:4px solid #c8860a;border-radius:4px;padding:16px 18px;font-family:Georgia,serif;font-size:16px;line-height:1.7;color:#201a10;white-space:pre-wrap;">${escapeHtml(message)}</div>
</td></tr>`
: `<tr><td style="padding:0 28px 22px;font-family:Arial,sans-serif;font-size:14px;color:#735a2b;font-style:italic;">They did not leave a message.</td></tr>`
const replyButton = email
? `<tr><td align="center" style="padding:0 28px 26px;">
<a href="mailto:${safeEmail}?subject=${encodeURIComponent('Welcome to the family of God')}" style="display:inline-block;background:#c8860a;color:#111111;font-family:Arial,sans-serif;font-size:14px;font-weight:700;letter-spacing:0.1em;text-transform:uppercase;text-decoration:none;padding:14px 30px;border-radius:4px;">Reply to ${escapeHtml(who)}</a>
</td></tr>`
: ''
const html = `<!doctype html><html><body style="margin:0;padding:0;background:#f5f1e8;">
<table width="100%" cellpadding="0" cellspacing="0" border="0" role="presentation" style="background:#f5f1e8;"><tr><td align="center" style="padding:28px 14px;">
<table width="100%" cellpadding="0" cellspacing="0" border="0" role="presentation" style="max-width:640px;background:#ffffff;border:1px solid #e1d3b2;border-radius:14px;overflow:hidden;">
<tr><td style="background:#111111;padding:30px 28px 26px;text-align:center;border-bottom:4px solid #c8860a;">
<div style="font-family:Arial,sans-serif;font-size:12px;letter-spacing:0.34em;text-transform:uppercase;color:#c8860a;">The Court of Heaven</div>
<div style="font-size:40px;line-height:1;margin:14px 0 6px;color:#c8860a;">&#9878;</div>
<h1 style="margin:0;font-family:Georgia,serif;font-size:30px;line-height:1.2;color:#f4ead5;">A Verdict Has Come In</h1>
<p style="margin:10px 0 0;font-family:Georgia,serif;font-style:italic;font-size:17px;color:#d9c9a3;">${escapeHtml(who)} pleaded guilty and accepted the pardon.</p>
</td></tr>
<tr><td style="padding:28px 28px 8px;">
<p style="margin:0 0 14px;font-family:Georgia,serif;font-size:17px;line-height:1.7;color:#201a10;">Someone just worked through <em>The Gospel, in a Courtroom</em> on your website, pleaded guilty, and says they prayed to receive Jesus as their Savior.</p>
<p style="margin:0 0 20px;font-family:Georgia,serif;font-style:italic;font-size:16px;line-height:1.6;color:#57452b;border-left:3px solid #c8860a;padding-left:14px;">&ldquo;There is joy in the presence of the angels of God over one sinner who repents.&rdquo; <span style="font-style:normal;font-size:13px;color:#8a6d35;">Luke 15:10</span></p>
</td></tr>
<tr><td style="padding:0 28px 22px;">
<table width="100%" cellpadding="0" cellspacing="0" border="0" role="presentation" style="border-top:2px solid #c8860a;">
${row('Name', safeName)}
${row('Email', email ? `<a href="mailto:${safeEmail}" style="color:#8f5f05;text-decoration:none;">${safeEmail}</a>` : '<span style="color:#735a2b;font-style:italic;">Not given</span>')}
${row('When', safeWhen, true)}
</table>
</td></tr>
${messageBlock}
${replyButton}
<tr><td style="padding:0 28px 26px;">
<div style="background:#f7f2e5;border:1px solid #e8dcc1;border-radius:10px;padding:18px 22px;">
<div style="font-family:Arial,sans-serif;font-size:12px;font-weight:700;letter-spacing:0.14em;text-transform:uppercase;color:#8a6d35;margin:0 0 12px;">Suggested follow-up</div>
<ol style="margin:0;padding-left:20px;">${steps}</ol>
</div>
</td></tr>
<tr><td style="background:#f7f2e5;border-top:1px solid #e8dcc1;padding:16px 28px;">
<p style="margin:0;font-family:Arial,sans-serif;font-size:12px;line-height:1.6;color:#735a2b;"><strong>Keep this email.</strong> It is the only record of this response. Nothing is stored on the website. ${email ? 'Replying to this message goes straight to them.' : ''}</p>
</td></tr>
</table>
</td></tr></table></body></html>`
return { subject, text, html }
}
// The warm, one-time confirmation the visitor receives at the address they gave.
function buildSalvationConfirmationEmail({ name }) {
const base = getCanonicalBaseUrl().replace(/\/$/, '')
const greeting = name ? `Hi ${name},` : 'Hi friend,'
const subject = 'We received your message. Welcome to the family of God'
const steps = [
['Read the Gospel of John.', 'Start there, then keep going through the New Testament.'],
['Talk to God every day.', 'Prayer is simply talking with him, in your own words.'],
['Tell someone.', 'Share your decision with a Christian friend or family member.'],
['Find a church.', 'Join a Bible-teaching church, and be baptized as a public sign of your faith (Acts 2:38).'],
]
const text = [
greeting,
'',
'Thank you for letting us know that you prayed to receive Jesus. We are rejoicing with you, and your message has been received.',
'Someone from our team will reach out to you personally soon.',
'',
'"Therefore, there is now no condemnation for those who are in Christ Jesus." (Romans 8:1)',
'You are no longer a defendant. You are a child of the Judge.',
'',
'WHAT TO DO NEXT',
...steps.map(([h, b], i) => `${i + 1}. ${h} ${b}`),
'',
`A good path through the podcast and studies: ${base}/start-here`,
`Questions? Ask here: ${base}/contact?source=salvation`,
'',
'Grace and peace,',
'Verse by Verse with Nate',
'',
`You are receiving this one-time email because this address was entered on ${base}/salvation. If that was not you, you can safely ignore it.`,
].join('\n')
const stepRows = steps
.map(([h, b], i) =>
`<tr><td valign="top" style="width:34px;padding:0 0 14px;"><div style="width:26px;height:26px;border-radius:13px;background:#c8860a;color:#111;font-family:Arial,sans-serif;font-size:13px;font-weight:700;line-height:26px;text-align:center;">${i + 1}</div></td>` +
`<td style="padding:2px 0 14px;font-family:Arial,sans-serif;font-size:15px;line-height:1.55;color:#201a10;"><strong>${escapeHtml(h)}</strong> ${escapeHtml(b)}</td></tr>`)
.join('')
const html = `<!doctype html><html><body style="margin:0;padding:0;background:#f5f1e8;">
<table width="100%" cellpadding="0" cellspacing="0" border="0" role="presentation" style="background:#f5f1e8;"><tr><td align="center" style="padding:28px 14px;">
<table width="100%" cellpadding="0" cellspacing="0" border="0" role="presentation" style="max-width:600px;background:#ffffff;border:1px solid #e1d3b2;border-radius:14px;overflow:hidden;">
<tr><td style="background:#111111;padding:30px 28px 26px;text-align:center;border-bottom:4px solid #c8860a;">
<div style="font-family:Arial,sans-serif;font-size:12px;letter-spacing:0.34em;text-transform:uppercase;color:#c8860a;">Verse by Verse with Nate</div>
<div style="width:54px;height:54px;border-radius:27px;background:#c8860a;color:#111111;font-family:Arial,sans-serif;font-size:30px;font-weight:700;line-height:54px;margin:16px auto 10px;">&#10003;</div>
<h1 style="margin:0;font-family:Georgia,serif;font-size:28px;line-height:1.25;color:#f4ead5;">Your message was received</h1>
<p style="margin:10px 0 0;font-family:Georgia,serif;font-style:italic;font-size:17px;color:#d9c9a3;">Welcome to the family of God.</p>
</td></tr>
<tr><td style="padding:30px 28px 6px;">
<p style="margin:0 0 16px;font-family:Georgia,serif;font-size:18px;line-height:1.6;color:#201a10;">${escapeHtml(greeting)}</p>
<p style="margin:0 0 16px;font-family:Georgia,serif;font-size:17px;line-height:1.7;color:#201a10;">Thank you for letting us know that you prayed to receive Jesus. We are rejoicing with you, and your message has been received.</p>
<p style="margin:0 0 22px;font-family:Georgia,serif;font-size:17px;line-height:1.7;color:#201a10;"><strong>Someone from our team will reach out to you personally soon.</strong></p>
</td></tr>
<tr><td style="padding:0 28px 26px;">
<div style="background:#fbf7ef;border-left:4px solid #c8860a;border-radius:4px;padding:18px 20px;">
<p style="margin:0 0 8px;font-family:Georgia,serif;font-style:italic;font-size:17px;line-height:1.6;color:#201a10;">&ldquo;Therefore, there is now no condemnation for those who are in Christ Jesus.&rdquo; <span style="font-style:normal;font-size:13px;color:#8a6d35;">Romans 8:1</span></p>
<p style="margin:0;font-family:Georgia,serif;font-size:15px;line-height:1.6;color:#57452b;">You are no longer a defendant. You are a child of the Judge.</p>
</div>
</td></tr>
<tr><td style="padding:0 28px 8px;">
<div style="font-family:Arial,sans-serif;font-size:12px;font-weight:700;letter-spacing:0.14em;text-transform:uppercase;color:#8a6d35;margin:0 0 14px;">What to do next</div>
<table width="100%" cellpadding="0" cellspacing="0" border="0" role="presentation">${stepRows}</table>
</td></tr>
<tr><td align="center" style="padding:6px 28px 8px;">
<a href="${escapeHtml(base)}/start-here" style="display:inline-block;background:#c8860a;color:#111111;font-family:Arial,sans-serif;font-size:14px;font-weight:700;letter-spacing:0.1em;text-transform:uppercase;text-decoration:none;padding:14px 28px;border-radius:4px;">Start Here</a>
</td></tr>
<tr><td align="center" style="padding:6px 28px 26px;font-family:Arial,sans-serif;font-size:14px;color:#57452b;">
Have a question? <a href="${escapeHtml(base)}/contact?source=salvation" style="color:#8f5f05;">Ask us here</a>, or just reply to this email.
</td></tr>
<tr><td style="padding:0 28px 28px;">
<p style="margin:0;font-family:Georgia,serif;font-size:16px;line-height:1.6;color:#201a10;">Grace and peace,<br><strong>Verse by Verse with Nate</strong></p>
</td></tr>
<tr><td style="background:#f7f2e5;border-top:1px solid #e8dcc1;padding:16px 28px;">
<p style="margin:0;font-family:Arial,sans-serif;font-size:12px;line-height:1.6;color:#735a2b;">You are receiving this one-time email because this address was entered at ${escapeHtml(base)}/salvation. If that was not you, you can safely ignore it.</p>
</td></tr>
</table>
</td></tr></table></body></html>`
return { subject, text, html }
}
export function register(app) { export function register(app) {
app.post('/api/salvation-response', rateLimit, async (req, res) => { app.post('/api/salvation-response', rateLimit, async (req, res) => {
const { name, email, message, _honey } = req.body ?? {} const { name, email, message, _honey } = req.body ?? {}
@@ -47,26 +244,8 @@ export function register(app) {
return return
} }
const when = new Date().toLocaleString('en-US', { dateStyle: 'medium', timeStyle: 'short' }) const when = new Date().toLocaleString('en-US', { weekday: 'long', year: 'numeric', month: 'long', day: 'numeric', hour: 'numeric', minute: '2-digit', timeZone: 'America/New_York', timeZoneName: 'short' })
const nameLine = safeName || 'Name not given' const { subject, text, html } = buildSalvationEmail({ name: safeName, email: safeEmail, message: safeMessage, when })
const emailLine = safeEmail || 'Email not given'
const text = [
'Someone responded to the gospel on the /salvation page (the courtroom presentation) and says they pleaded guilty, accepted the pardon, and prayed to receive Jesus as Savior.',
'',
`Name: ${nameLine}`,
`Email: ${emailLine}`,
`When: ${when}`,
'',
safeMessage ? `Their message:\n${safeMessage}` : 'They did not leave a message.',
'',
safeEmail ? 'You can reply directly to this email to reach them.' : 'They did not leave an email address, so there is no way to reply.',
].join('\n')
const html = `<div style="font-family:Georgia,serif;max-width:560px;line-height:1.6;color:#1a1208">
<p><strong>Someone responded to the gospel</strong> on the /salvation page and says they prayed to receive Jesus as Savior.</p>
<p><strong>Name:</strong> ${escapeHtml(nameLine)}<br><strong>Email:</strong> ${escapeHtml(emailLine)}<br><strong>When:</strong> ${escapeHtml(when)}</p>
${safeMessage ? `<p><strong>Their message:</strong></p><blockquote style="margin:0;padding-left:12px;border-left:3px solid #b8860b">${escapeHtml(safeMessage).replace(/\n/g, '<br>')}</blockquote>` : '<p><em>They did not leave a message.</em></p>'}
<p style="color:#6b5a30">${safeEmail ? 'You can reply directly to this email to reach them.' : 'They did not leave an email address, so there is no way to reply.'}</p>
</div>`
try { try {
const resend = new Resend(process.env.RESEND_API_KEY) const resend = new Resend(process.env.RESEND_API_KEY)
@@ -77,13 +256,37 @@ ${safeMessage ? `<p><strong>Their message:</strong></p><blockquote style="margin
from: getResendFromAddress(), from: getResendFromAddress(),
to: [getResendInboxAddress()], to: [getResendInboxAddress()],
...(safeEmail ? { replyTo: safeEmail } : {}), ...(safeEmail ? { replyTo: safeEmail } : {}),
subject: 'Someone prayed to receive Christ on your salvation page', subject,
tags: [{ name: 'flow', value: 'salvation-response' }], tags: [{ name: 'flow', value: 'salvation-response' }],
text, text,
html, html,
}, },
}) })
res.json({ ok: true })
// Best effort: a failed or skipped confirmation must never turn a delivered decision into an error.
let confirmationSent = false
if (safeEmail && mayConfirm(safeEmail)) {
try {
const confirmation = buildSalvationConfirmationEmail({ name: safeName })
await sendResendEmailWithRetry({
resend,
context: 'salvation-confirmation',
payload: {
from: getResendFromAddress(),
to: [safeEmail],
replyTo: getResendReplyToAddress(),
subject: confirmation.subject,
tags: [{ name: 'flow', value: 'salvation-confirmation' }],
text: confirmation.text,
html: confirmation.html,
},
})
confirmationSent = true
} catch (confirmErr) {
console.error('[salvation] confirmation email failed:', confirmErr?.message ?? confirmErr)
}
}
res.json({ ok: true, confirmationSent })
} catch (err) { } catch (err) {
console.error('[salvation] email failed:', err?.message ?? err) console.error('[salvation] email failed:', err?.message ?? err)
res.status(502).json({ message: 'We could not send this right now.' }) res.status(502).json({ message: 'We could not send this right now.' })