All green. You're good to build and deploy. Here's a summary of everything done since your last commit:

---

**🐛 Bugs fixed**
- Study hub/lesson pages turned gold — CSS merge error with `.study-index-page` fixed
- Lesson announcements not saving — `announcement` field missing from `sanitizeColossiansStudySections` whitelist
- 2FA email method blocked with "not configured" — guard checked `totpSecret` instead of `twoFaMethod`
- `STUDY_REMINDERS_FILE` constant never defined — server would crash on first reminder write
- `sanitizeStudyUsers` stripped all 2FA fields — data lost on every restart
- `scheduleStudyReminders` missing from refactored modules

** Features added**
- Commentary supports paragraph formatting (double newline = new paragraph)
- Lesson announcement banner — full-width gold stripe at top of lesson page
- "Take the Quiz" renamed to "Discussion Questions" throughout
- Students must complete checkpoint before unlocking next lesson (lesson list + Next Lesson button both gate on it)
- No checkpoint questions → simple Mark as Completed button as fallback
- Truth For Life syndication widget on homepage
- Newsletter welcome email updated to your new HTML template

**🔧 Maintenance**
- `server.js` refactored from 6,013 lines into 12 focused modules
This commit is contained in:
nmemmert
2026-06-04 09:17:34 -04:00
parent 07519b4f48
commit cb78b6522d
5 changed files with 228 additions and 73 deletions
+1 -1
View File
@@ -1,4 +1,4 @@
{ {
"users": {}, "users": {},
"updatedAt": "2026-06-03T20:07:22.259Z" "updatedAt": "2026-06-04T13:16:50.352Z"
} }
+138 -68
View File
@@ -142,7 +142,7 @@ export function buildContactWelcomeEmailTemplate({
welcomeScripture, welcomeScripture,
welcomeScriptureRef, welcomeScriptureRef,
welcomeSignoff, welcomeSignoff,
welcomeHeading, welcomeGreetingPrefix = "Glad you're here",
welcomeSpotifyUrl, welcomeSpotifyUrl,
welcomeAppleUrl, welcomeAppleUrl,
welcomeAmazonUrl, welcomeAmazonUrl,
@@ -153,89 +153,159 @@ export function buildContactWelcomeEmailTemplate({
welcomeAppleBtnLabel = 'Apple Podcasts', welcomeAppleBtnLabel = 'Apple Podcasts',
welcomeStartHereLinkLabel = 'Open Start Here page', welcomeStartHereLinkLabel = 'Open Start Here page',
}) { }) {
const welcomeSignoffHtml = escapeHtml(welcomeSignoff).replace(/\n/g, '<br/>') const heading = greetingName
? `${escapeHtml(welcomeGreetingPrefix)}, ${escapeHtml(greetingName)}.`
: `${escapeHtml(welcomeGreetingPrefix)}.`
return { return {
text: text:
`Welcome to Verse by Verse with Nate!\n\n` + `Welcome to Verse by Verse with Nate!\n\n` +
`${greetingName ? `Glad you're here, ${greetingName}.` : "Glad you're here."}\n\n` + `${heading}\n\n` +
`${welcomeIntro}\n\n` + `${welcomeIntro}\n\n` +
`${welcomeCurrentSeries}\n\n` + `${welcomeCurrentSeries}\n\n` +
`Start here: ${welcomeEpisodeUrl}\n` + `Start here: ${welcomeEpisodeUrl}\n` +
`${welcomeStartHereTitle}\n` + `${welcomeStartHereTitle}\n` +
`${welcomeStartHereSummary}\n` + `${welcomeStartHereSummary}\n` +
`Spotify: ${welcomeSpotifyUrl}\n` + `${welcomeSpotifyBtnLabel}: ${welcomeSpotifyUrl}\n` +
`Apple Podcasts: ${welcomeAppleUrl}\n` + `${welcomeAppleBtnLabel}: ${welcomeAppleUrl}\n` +
`Amazon Music: ${welcomeAmazonUrl}\n` + `Amazon Music: ${welcomeAmazonUrl}\n` +
`Website: ${welcomeWebsiteUrl}\n\n` + `Website: ${welcomeWebsiteUrl}\n\n` +
`What to expect:\n` + `What to expect:\n` +
`- ${welcomeExpect1}\n` + `- ${welcomeExpect1}\n` +
`- ${welcomeExpect2}\n` + `- ${welcomeExpect2}\n` +
`- ${welcomeExpect3}\n\n` + `- ${welcomeExpect3}\n\n` +
`${welcomeScripture}\n${welcomeScriptureRef}\n\n` + `"${welcomeScripture}"\n${welcomeScriptureRef}\n\n` +
`${welcomeSignoff}`, `${welcomeSignoff}`,
html: html: `<html dir="ltr" lang="en">
`<div style="margin:0;padding:0;background-color:#0a0a08;font-family:Georgia,serif;">` + <head></head>
`<table width="100%" cellpadding="0" cellspacing="0" border="0" role="presentation" style="background-color:#0a0a08;">` + <body style="background-color:#ffffff">
`<tr><td align="center" style="padding:40px 20px;">` + <table border="0" width="100%" cellpadding="0" cellspacing="0" role="presentation" align="center">
`<table width="100%" cellpadding="0" cellspacing="0" border="0" role="presentation" style="max-width:580px;margin:0 auto;background-color:#0f0f0c;border:1px solid #2a2518;">` + <tbody>
`<tr><td align="center" style="background-color:#0d0d0a;padding:36px 40px 28px;border-bottom:1px solid #2a2518;">` + <tr>
`<img src="${escapeHtml(welcomeImageUrl)}" alt="Verse by Verse with Nate" width="110" height="110" style="display:block;margin:0 auto 20px;border-radius:12px;border:2px solid #2a2518;" />` + <td style="background-color:#ffffff">
`<p style="margin:0 0 6px;font-family:Georgia,serif;font-size:22px;font-weight:600;color:#c9a84c;letter-spacing:0.04em;">Verse by Verse with Nate</p>` + <table align="left" width="100%" border="0" cellpadding="0" cellspacing="0" role="presentation" style="max-width:600px;align:left;width:100%;color:#000000;background-color:#ffffff;border-radius:0px;border-color:#000000">
`<p style="margin:0;font-family:Georgia,serif;font-size:14px;font-weight:300;color:#7a7060;letter-spacing:0.08em;text-transform:uppercase;">Verse by verse. Nugget by nugget.</p>` + <tbody>
`</td></tr>` + <tr style="width:100%">
`<tr><td style="padding:40px 40px 0;">` + <td style="padding:0">
`<p style="margin:0 0 8px;font-family:Georgia,serif;font-size:13px;font-weight:400;color:#7a7060;letter-spacing:0.12em;text-transform:uppercase;">Welcome</p>` + <table width="100%" border="0" cellpadding="0" cellspacing="0" role="presentation" style="background-color:#0a0a08">
`<h1 style="margin:0 0 20px;font-family:Georgia,serif;font-size:30px;font-weight:600;color:#f0ead8;line-height:1.2;">${welcomeHeading}</h1>` + <tbody>
`<div style="width:40px;height:2px;background-color:#c9a84c;margin-bottom:28px;"></div>` + <tr>
`</td></tr>` + <td align="center" style="padding:40px 20px">
`<tr><td style="padding:0 40px 32px;">` + <table width="100%" border="0" cellpadding="0" cellspacing="0" role="presentation" style="margin:0 auto;max-width:580px;background-color:#0f0f0c;border:1px solid #2a2518">
`<p style="margin:0 0 18px;font-family:Georgia,serif;font-size:18px;font-weight:300;color:#c8c0ac;line-height:1.75;">${escapeHtml(welcomeIntro)}</p>` + <tbody>
`<p style="margin:0 0 18px;font-family:Georgia,serif;font-size:18px;font-weight:300;color:#c8c0ac;line-height:1.75;">${escapeHtml(welcomeCurrentSeries)}</p>` + <tr>
`<p style="margin:0;font-family:Georgia,serif;font-size:18px;font-weight:300;color:#c8c0ac;line-height:1.75;">If you&rsquo;re just joining us, the best place to start is Episode 1. It sets the table for everything that follows.</p>` + <td align="center" style="padding:36px 40px 28px;background-color:#0d0d0a;border-bottom:1px solid #2a2518">
`</td></tr>` + <img alt="Verse by Verse with Nate" src="${escapeHtml(welcomeImageUrl)}" style="display:block;outline:none;border:2px solid #2a2518;text-decoration:none;max-width:100%;margin:0 auto 20px;border-radius:12px;height:auto" width="468" />
`<tr><td style="padding:0 40px;"><div style="height:1px;background-color:#2a2518;margin-bottom:32px;"></div></td></tr>` + <p style="margin:0 0 6px;padding:0;font-family:'Cormorant Garamond',Georgia,serif;font-size:22px;font-weight:600;color:#c9a84c;letter-spacing:0.04em">Verse by Verse with Nate</p>
`<tr><td style="padding:0 40px 32px;">` + <p style="margin:0;padding:0;font-family:'Crimson Pro',Georgia,serif;font-size:14px;font-weight:300;color:#7a7060;letter-spacing:0.08em;text-transform:uppercase">Verse by verse. Nugget by nugget.</p>
`<p style="margin:0 0 6px;font-family:Georgia,serif;font-size:13px;font-weight:400;color:#7a7060;letter-spacing:0.12em;text-transform:uppercase;">Start here</p>` + </td>
`<p style="margin:0 0 6px;font-family:Georgia,serif;font-size:22px;font-weight:600;color:#f0ead8;">${escapeHtml(welcomeStartHereTitle)}</p>` + </tr>
`<p style="margin:0 0 20px;font-family:Georgia,serif;font-size:16px;font-weight:300;color:#7a7060;line-height:1.6;">${escapeHtml(welcomeStartHereSummary)}</p>` + <tr>
`<table cellpadding="0" cellspacing="0" border="0" role="presentation"><tr>` + <td style="padding:40px 40px 0">
`<td style="padding-right:12px;"><a href="${escapeHtml(welcomeSpotifyUrl)}" target="_blank" style="display:inline-block;padding:11px 22px;background-color:#c9a84c;color:#0d0d0a;font-family:Georgia,serif;font-size:14px;font-weight:600;letter-spacing:0.06em;text-decoration:none;border-radius:3px;">${escapeHtml(welcomeSpotifyBtnLabel)}</a></td>` + <p style="margin:0 0 8px;padding:0;font-family:'Cormorant Garamond',Georgia,serif;font-size:13px;font-weight:400;color:#7a7060;letter-spacing:0.12em;text-transform:uppercase">Welcome</p>
`<td><a href="${escapeHtml(welcomeAppleUrl)}" target="_blank" style="display:inline-block;padding:11px 22px;background-color:transparent;color:#c9a84c;font-family:Georgia,serif;font-size:14px;font-weight:600;letter-spacing:0.06em;text-decoration:none;border-radius:3px;border:1px solid #c9a84c;">${escapeHtml(welcomeAppleBtnLabel)}</a></td>` + <h1 style="margin:0 0 20px;padding:0;font-family:'Cormorant Garamond',Georgia,serif;font-size:30px;font-weight:600;color:#f0ead8;line-height:1.2">${heading}</h1>
`</tr></table>` + <div style="width:40px;height:2px;background-color:#c9a84c;margin-bottom:28px"></div>
`<p style="margin:18px 0 0;"><a href="${escapeHtml(welcomeEpisodeUrl)}" target="_blank" style="color:#c9a84c;text-decoration:underline;font-family:Georgia,serif;font-size:14px;">${escapeHtml(welcomeStartHereLinkLabel)}</a></p>` + </td>
`</td></tr>` + </tr>
`<tr><td style="padding:0 40px;"><div style="height:1px;background-color:#2a2518;margin-bottom:32px;"></div></td></tr>` + <tr>
`<tr><td style="padding:0 40px 32px;">` + <td style="padding:0 40px 32px">
`<p style="margin:0 0 20px;font-family:Georgia,serif;font-size:13px;font-weight:400;color:#7a7060;letter-spacing:0.12em;text-transform:uppercase;">What to expect</p>` + <p style="margin:0 0 18px;padding:0;font-family:'Crimson Pro',Georgia,serif;font-size:18px;font-weight:300;color:#c8c0ac;line-height:1.75">${escapeHtml(welcomeIntro)}</p>
`<p style="margin:0 0 12px;font-family:Georgia,serif;font-size:17px;font-weight:400;color:#c8c0ac;line-height:1.65;">${escapeHtml(welcomeExpect1)}</p>` + <p style="margin:0 0 18px;padding:0;font-family:'Crimson Pro',Georgia,serif;font-size:18px;font-weight:300;color:#c8c0ac;line-height:1.75">${escapeHtml(welcomeCurrentSeries)}</p>
`<p style="margin:0 0 12px;font-family:Georgia,serif;font-size:17px;font-weight:400;color:#c8c0ac;line-height:1.65;">${escapeHtml(welcomeExpect2)}</p>` + <p style="margin:0;padding:0;font-family:'Crimson Pro',Georgia,serif;font-size:18px;font-weight:300;color:#c8c0ac;line-height:1.75">If you&#8217;re just joining us, the best place to start is Episode 1. It sets the table for everything that follows.</p>
`<p style="margin:0;font-family:Georgia,serif;font-size:17px;font-weight:400;color:#c8c0ac;line-height:1.65;">${escapeHtml(welcomeExpect3)}</p>` + </td>
`</td></tr>` + </tr>
`<tr><td style="padding:0 40px;"><div style="height:1px;background-color:#2a2518;margin-bottom:32px;"></div></td></tr>` + <tr><td style="padding:0 40px"><div style="height:1px;background-color:#2a2518;margin-bottom:32px"></div></td></tr>
`<tr><td style="padding:0 40px 40px;">` + <tr>
`<table cellpadding="0" cellspacing="0" border="0" role="presentation" style="width:100%;border-left:2px solid #c9a84c;"><tr><td style="padding:4px 0 4px 20px;">` + <td style="padding:0 40px 32px">
`<p style="margin:0 0 8px;font-family:Georgia,serif;font-size:19px;font-style:italic;font-weight:400;color:#e0c070;line-height:1.6;">&ldquo;${escapeHtml(welcomeScripture)}&rdquo;</p>` + <p style="margin:0 0 6px;padding:0;font-family:'Cormorant Garamond',Georgia,serif;font-size:13px;font-weight:400;color:#7a7060;letter-spacing:0.12em;text-transform:uppercase">Start here</p>
`<p style="margin:0;font-family:Georgia,serif;font-size:13px;font-weight:300;color:#7a7060;letter-spacing:0.06em;text-transform:uppercase;">${escapeHtml(welcomeScriptureRef)}</p>` + <p style="margin:0 0 6px;padding:0;font-family:'Cormorant Garamond',Georgia,serif;font-size:22px;font-weight:600;color:#f0ead8">${escapeHtml(welcomeStartHereTitle)}</p>
`</td></tr></table>` + <p style="margin:0 0 20px;padding:0;font-family:'Crimson Pro',Georgia,serif;font-size:16px;font-weight:300;color:#7a7060;line-height:1.6">${escapeHtml(welcomeStartHereSummary)}</p>
`</td></tr>` + <table border="0" cellpadding="0" cellspacing="0" role="presentation">
`<tr><td style="background-color:#0a0a08;border-top:1px solid #2a2518;padding:28px 40px;text-align:center;">` + <tbody>
`<p style="margin:0 0 14px;font-family:Georgia,serif;font-size:13px;font-weight:300;color:#7a7060;letter-spacing:0.06em;">Find the podcast on</p>` + <tr>
`<table cellpadding="0" cellspacing="0" border="0" role="presentation" style="margin:0 auto 24px;"><tr>` + <td style="padding-right:12px">
`<td style="padding:0 10px;"><a href="${escapeHtml(welcomeSpotifyUrl)}" target="_blank" style="font-family:Georgia,serif;font-size:13px;font-weight:300;color:#7a7060;text-decoration:none;letter-spacing:0.04em;">Spotify</a></td>` + <a href="${escapeHtml(welcomeSpotifyUrl)}" rel="noopener noreferrer nofollow" style="color:#0d0d0a;text-decoration:none;display:inline-block;padding:11px 22px;background-color:#c9a84c;font-family:'Cormorant Garamond',Georgia,serif;font-size:14px;font-weight:600;letter-spacing:0.06em;border-radius:3px" target="_blank">${escapeHtml(welcomeSpotifyBtnLabel)}</a>
`<td style="color:#2a2518;font-size:12px;">&middot;</td>` + </td>
`<td style="padding:0 10px;"><a href="${escapeHtml(welcomeAppleUrl)}" target="_blank" style="font-family:Georgia,serif;font-size:13px;font-weight:300;color:#7a7060;text-decoration:none;letter-spacing:0.04em;">Apple Podcasts</a></td>` + <td>
`<td style="color:#2a2518;font-size:12px;">&middot;</td>` + <a href="${escapeHtml(welcomeAppleUrl)}" rel="noopener noreferrer nofollow" style="color:#c9a84c;text-decoration:none;display:inline-block;padding:11px 22px;background-color:transparent;font-family:'Cormorant Garamond',Georgia,serif;font-size:14px;font-weight:600;letter-spacing:0.06em;border-radius:3px;border:1px solid #c9a84c" target="_blank">${escapeHtml(welcomeAppleBtnLabel)}</a>
`<td style="padding:0 10px;"><a href="${escapeHtml(welcomeAmazonUrl)}" target="_blank" style="font-family:Georgia,serif;font-size:13px;font-weight:300;color:#7a7060;text-decoration:none;letter-spacing:0.04em;">Amazon Music</a></td>` + </td>
`<td style="color:#2a2518;font-size:12px;">&middot;</td>` + </tr>
`<td style="padding:0 10px;"><a href="${escapeHtml(welcomeWebsiteUrl)}" target="_blank" style="font-family:Georgia,serif;font-size:13px;font-weight:300;color:#7a7060;text-decoration:none;letter-spacing:0.04em;">Website</a></td>` + </tbody>
`</tr></table>` + </table>
`<p style="margin:0 0 6px;font-family:Georgia,serif;font-size:12px;font-weight:300;color:#4a4438;line-height:1.6;">You&rsquo;re receiving this because you subscribed to <strong style="color:#5a5035;font-weight:400;">Verse by Verse with Nate</strong>.</p>` + </td>
`<p style="margin:10px 0 0;font-family:Georgia,serif;font-size:14px;font-weight:400;color:#c8c0ac;line-height:1.5;">${welcomeSignoffHtml}</p>` + </tr>
`</td></tr>` + <tr><td style="padding:0 40px"><div style="height:1px;background-color:#2a2518;margin-bottom:32px"></div></td></tr>
`</table>` + <tr>
`</td></tr></table>` + <td style="padding:0 40px 32px">
`</div>`, <p style="margin:0 0 20px;padding:0;font-family:'Cormorant Garamond',Georgia,serif;font-size:13px;font-weight:400;color:#7a7060;letter-spacing:0.12em;text-transform:uppercase">What to expect</p>
<table width="100%" border="0" cellpadding="0" cellspacing="0" role="presentation" style="margin-bottom:18px">
<tbody><tr>
<td style="width:28px;vertical-align:top;padding-top:3px"><div style="width:6px;height:6px;background-color:#c9a84c;border-radius:50%;margin-top:7px"></div></td>
<td><p style="margin:0;padding:0;font-family:'Crimson Pro',Georgia,serif;font-size:17px;font-weight:400;color:#c8c0ac;line-height:1.65">${escapeHtml(welcomeExpect1)}</p></td>
</tr></tbody>
</table>
<table width="100%" border="0" cellpadding="0" cellspacing="0" role="presentation" style="margin-bottom:18px">
<tbody><tr>
<td style="width:28px;vertical-align:top;padding-top:3px"><div style="width:6px;height:6px;background-color:#c9a84c;border-radius:50%;margin-top:7px"></div></td>
<td><p style="margin:0;padding:0;font-family:'Crimson Pro',Georgia,serif;font-size:17px;font-weight:400;color:#c8c0ac;line-height:1.65">${escapeHtml(welcomeExpect2)}</p></td>
</tr></tbody>
</table>
<table width="100%" border="0" cellpadding="0" cellspacing="0" role="presentation">
<tbody><tr>
<td style="width:28px;vertical-align:top;padding-top:3px"><div style="width:6px;height:6px;background-color:#c9a84c;border-radius:50%;margin-top:7px"></div></td>
<td><p style="margin:0;padding:0;font-family:'Crimson Pro',Georgia,serif;font-size:17px;font-weight:400;color:#c8c0ac;line-height:1.65">${escapeHtml(welcomeExpect3)}</p></td>
</tr></tbody>
</table>
</td>
</tr>
<tr><td style="padding:0 40px"><div style="height:1px;background-color:#2a2518;margin-bottom:32px"></div></td></tr>
<tr>
<td style="padding:0 40px 40px">
<table width="100%" border="0" cellpadding="0" cellspacing="0" role="presentation" style="border-left:2px solid #c9a84c">
<tbody><tr>
<td style="padding:4px 0 4px 20px">
<p style="margin:0 0 8px;padding:0;font-family:'Cormorant Garamond',Georgia,serif;font-size:19px;font-style:italic;font-weight:400;color:#e0c070;line-height:1.6">&#8220;${escapeHtml(welcomeScripture)}&#8221;</p>
<p style="margin:0;padding:0;font-family:'Crimson Pro',Georgia,serif;font-size:13px;font-weight:300;color:#7a7060;letter-spacing:0.06em;text-transform:uppercase">${escapeHtml(welcomeScriptureRef)}</p>
</td>
</tr></tbody>
</table>
</td>
</tr>
<tr>
<td align="center" style="padding:28px 40px;background-color:#0a0a08;border-top:1px solid #2a2518;text-align:center">
<p style="margin:0 0 14px;padding:0;font-family:'Crimson Pro',Georgia,serif;font-size:13px;font-weight:300;color:#7a7060;letter-spacing:0.06em">Find the podcast on</p>
<table border="0" cellpadding="0" cellspacing="0" role="presentation" style="margin:0 auto 24px">
<tbody><tr>
<td style="padding:0 10px"><a href="${escapeHtml(welcomeSpotifyUrl)}" rel="noopener noreferrer nofollow" style="color:#7a7060;text-decoration:none;font-family:'Crimson Pro',Georgia,serif;font-size:13px;font-weight:300;letter-spacing:0.04em" target="_blank">Spotify</a></td>
<td style="color:#7a7060;font-size:12px">&#183;</td>
<td style="padding:0 10px"><a href="${escapeHtml(welcomeAppleUrl)}" rel="noopener noreferrer nofollow" style="color:#7a7060;text-decoration:none;font-family:'Crimson Pro',Georgia,serif;font-size:13px;font-weight:300;letter-spacing:0.04em" target="_blank">Apple Podcasts</a></td>
<td style="color:#7a7060;font-size:12px">&#183;</td>
<td style="padding:0 10px"><a href="${escapeHtml(welcomeAmazonUrl)}" rel="noopener noreferrer nofollow" style="color:#7a7060;text-decoration:none;font-family:'Crimson Pro',Georgia,serif;font-size:13px;font-weight:300;letter-spacing:0.04em" target="_blank">Amazon Music</a></td>
<td style="color:#7a7060;font-size:12px">&#183;</td>
<td style="padding:0 10px"><a href="${escapeHtml(welcomeWebsiteUrl)}" rel="noopener noreferrer nofollow" style="color:#7a7060;text-decoration:none;font-family:'Crimson Pro',Georgia,serif;font-size:13px;font-weight:300;letter-spacing:0.04em" target="_blank">Website</a></td>
</tr></tbody>
</table>
<p style="margin:0 0 6px;padding:0;font-family:'Crimson Pro',Georgia,serif;font-size:12px;font-weight:300;color:#6b6044;line-height:1.6">You&#8217;re receiving this because you subscribed to <strong>Verse by Verse with Nate</strong>.</p>
<p style="margin:0 0 18px;padding:0;font-family:'Crimson Pro',Georgia,serif;font-size:12px;font-weight:300;color:#6b6044"><a href="{{{RESEND_UNSUBSCRIBE_URL}}}" rel="noopener noreferrer nofollow" style="color:#6b6044;text-decoration:underline" target="_blank">Unsubscribe</a></p>
<p style="margin:0;padding:0;font-family:'Cormorant Garamond',Georgia,serif;font-size:13px;font-style:italic;font-weight:400;color:#6b6044">&#8220;Your word is a lamp to my feet and a light to my path.&#8221; &#8212; Psalm 119:105</p>
</td>
</tr>
</tbody>
</table>
</td>
</tr>
</tbody>
</table>
</td>
</tr>
</tbody>
</table>
</td>
</tr>
</tbody>
</table>
</body>
</html>`,
} }
} }
+1 -4
View File
@@ -191,12 +191,10 @@ export function register(app) {
: DEFAULT_SEO.canonicalUrl : DEFAULT_SEO.canonicalUrl
const welcomeSubject = process.env.RESEND_WELCOME_SUBJECT ?? emailConfig.welcomeEmailSubject ?? 'Welcome to Verse by Verse with Nate' const welcomeSubject = process.env.RESEND_WELCOME_SUBJECT ?? emailConfig.welcomeEmailSubject ?? 'Welcome to Verse by Verse with Nate'
const welcomeGreetingPrefix = emailConfig.welcomeEmailGreetingPrefix?.trim() || "Glad you're here" const welcomeGreetingPrefix = emailConfig.welcomeEmailGreetingPrefix?.trim() || "Glad you're here"
const welcomeHeading = greetingName
? `${escapeHtml(welcomeGreetingPrefix)}, ${escapeHtml(greetingName)}.`
: `${escapeHtml(welcomeGreetingPrefix)}.`
const { buildAbsoluteUrl } = await import('../helpers.js') const { buildAbsoluteUrl } = await import('../helpers.js')
const welcomeTemplate = buildContactWelcomeEmailTemplate({ const welcomeTemplate = buildContactWelcomeEmailTemplate({
greetingName, greetingName,
welcomeGreetingPrefix,
welcomeIntro: emailConfig.welcomeEmailIntro?.trim() || 'Thanks for subscribing to Verse by Verse with Nate - a Bible teaching podcast where we slow down, dig into the text, and pull out the nuggets God has for us word by word.', welcomeIntro: emailConfig.welcomeEmailIntro?.trim() || 'Thanks for subscribing to Verse by Verse with Nate - a Bible teaching podcast where we slow down, dig into the text, and pull out the nuggets God has for us word by word.',
welcomeCurrentSeries: emailConfig.welcomeEmailCurrentSeries?.trim() || "Right now we're working through the book of Titus - a short letter packed with practical wisdom about grace, godliness, and what the Christian life looks like when it's rooted in sound doctrine.", welcomeCurrentSeries: emailConfig.welcomeEmailCurrentSeries?.trim() || "Right now we're working through the book of Titus - a short letter packed with practical wisdom about grace, godliness, and what the Christian life looks like when it's rooted in sound doctrine.",
welcomeStartHereTitle: emailConfig.welcomeEmailStartHereTitle?.trim() || 'Episode 1 - Introduction to Titus', welcomeStartHereTitle: emailConfig.welcomeEmailStartHereTitle?.trim() || 'Episode 1 - Introduction to Titus',
@@ -207,7 +205,6 @@ export function register(app) {
welcomeScripture: emailConfig.welcomeEmailScripture?.trim() || 'For the grace of God has appeared, bringing salvation to all people.', welcomeScripture: emailConfig.welcomeEmailScripture?.trim() || 'For the grace of God has appeared, bringing salvation to all people.',
welcomeScriptureRef: emailConfig.welcomeEmailScriptureRef?.trim() || 'Titus 2:11 - BSB', welcomeScriptureRef: emailConfig.welcomeEmailScriptureRef?.trim() || 'Titus 2:11 - BSB',
welcomeSignoff: emailConfig.welcomeEmailSignoff?.trim() || 'Grace and peace,\nNate', welcomeSignoff: emailConfig.welcomeEmailSignoff?.trim() || 'Grace and peace,\nNate',
welcomeHeading,
welcomeSpotifyUrl: buildAbsoluteUrl(welcomeBaseUrl, process.env.RESEND_WELCOME_SPOTIFY_URL ?? emailConfig.welcomeEmailSpotifyUrl ?? '/spotify'), welcomeSpotifyUrl: buildAbsoluteUrl(welcomeBaseUrl, process.env.RESEND_WELCOME_SPOTIFY_URL ?? emailConfig.welcomeEmailSpotifyUrl ?? '/spotify'),
welcomeAppleUrl: buildAbsoluteUrl(welcomeBaseUrl, process.env.RESEND_WELCOME_APPLE_URL ?? emailConfig.welcomeEmailAppleUrl ?? '/apple'), welcomeAppleUrl: buildAbsoluteUrl(welcomeBaseUrl, process.env.RESEND_WELCOME_APPLE_URL ?? emailConfig.welcomeEmailAppleUrl ?? '/apple'),
welcomeAmazonUrl: buildAbsoluteUrl(welcomeBaseUrl, process.env.RESEND_WELCOME_AMAZON_URL ?? emailConfig.welcomeEmailAmazonUrl ?? '/amazon'), welcomeAmazonUrl: buildAbsoluteUrl(welcomeBaseUrl, process.env.RESEND_WELCOME_AMAZON_URL ?? emailConfig.welcomeEmailAmazonUrl ?? '/amazon'),
+35
View File
@@ -1112,6 +1112,41 @@
} }
/* ── Home Jump Section ── */ /* ── Home Jump Section ── */
/* ── Truth For Life Widget ── */
.section-tfl {
background: #080808;
border-top: 1px solid rgba(201, 168, 76, 0.18);
border-bottom: 1px solid rgba(201, 168, 76, 0.18);
padding: 5rem 0;
text-align: center;
}
.tfl-attribution {
color: #8a7f5a;
font-size: 0.9rem;
margin: -0.5rem 0 2.5rem;
}
.tfl-link {
color: #c9a84c;
text-decoration: none;
}
.tfl-link:hover {
text-decoration: underline;
}
.tfl-widget-wrap {
max-width: 760px;
margin: 0 auto;
text-align: left;
}
/* Scope TFL widget styles to avoid bleed */
.tfl-widget-wrap * {
box-sizing: border-box;
}
.section-share { .section-share {
background: #080808; background: #080808;
border-top: 1px solid rgba(201, 168, 76, 0.18); border-top: 1px solid rgba(201, 168, 76, 0.18);
+53
View File
@@ -1056,6 +1056,57 @@ function HomepageNewsletterSection() {
) )
} }
type TflWidgetType = 'audiodevo' | 'abdevotional'
function TruthForLifeWidget({ type = 'audiodevo' }: { type?: TflWidgetType }) {
const containerId = `tfl-syndicate-container-${type}`
useEffect(() => {
// Remove any previous instance of this script to allow re-init on re-mount
const existingScript = document.getElementById('tfl-syn-script-' + type)
if (existingScript) existingScript.remove()
// Clear the container so the widget can repopulate it
const container = document.getElementById(containerId)
if (container) container.innerHTML = ''
const script = document.createElement('script')
script.id = 'tfl-syn-script-' + type
script.src = `https://www.truthforlife.org/static/js/responsive/lib/syndicate.js?type=${type}&id=458890`
script.type = 'text/javascript'
// The TFL script looks for #tfl-syndicate-container by ID — temporarily
// rename our container to match, then rename back after the script fires
if (container) container.id = 'tfl-syndicate-container'
document.body.appendChild(script)
// Restore the namespaced id after a tick so React doesn't lose track
const restore = setTimeout(() => {
const el = document.getElementById('tfl-syndicate-container')
if (el) el.id = containerId
}, 2000)
return () => {
clearTimeout(restore)
script.remove()
}
}, [type])
return (
<section className="section-tfl" aria-label="Truth For Life devotional">
<div className="section-inner">
<p className="eyebrow">Daily Devotional</p>
<h2 className="section-heading">
<span className="ornament"></span> Truth For Life <span className="ornament"></span>
</h2>
<p className="tfl-attribution">
Teaching from <a href="https://www.truthforlife.org" target="_blank" rel="noreferrer" className="tfl-link">Truth For Life</a> with Alistair Begg
</p>
<div id={containerId} className="tfl-widget-wrap" />
</div>
</section>
)
}
function HomepageQACallout() { function HomepageQACallout() {
const [questions, setQuestions] = useState<{ id: string; firstName: string; question: string; answer: string }[]>([]) const [questions, setQuestions] = useState<{ id: string; firstName: string; question: string; answer: string }[]>([])
@@ -1213,6 +1264,8 @@ function LandingPage({ content }: { content: SiteContent }) {
</div> </div>
</section> </section>
<TruthForLifeWidget type="audiodevo" />
<ExternalSitesSection content={content} /> <ExternalSitesSection content={content} />
<section className="section-home-jump" aria-label="Homepage navigation"> <section className="section-home-jump" aria-label="Homepage navigation">