From a8f6024234dc1080ec70a851d166309bd5bd7abf Mon Sep 17 00:00:00 2001
From: nmemmert
Date: Tue, 30 Jun 2026 09:26:00 -0400
Subject: [PATCH] Fix Truth For Life devotional widget not rendering
Replaced broken srcdoc iframe approach with direct script injection so
the TFL syndicate.js runs in page context with access to the DOM it needs.
Also added truthforlife.org and ajax.googleapis.com to CSP script-src,
which was silently blocking the widget and its jQuery dependency.
Co-Authored-By: Claude Sonnet 4.6
---
server.js | 2 +-
src/App.tsx | 242 +++++++---------------------------------------------
2 files changed, 31 insertions(+), 213 deletions(-)
diff --git a/server.js b/server.js
index a92825c..50100eb 100644
--- a/server.js
+++ b/server.js
@@ -68,7 +68,7 @@ app.use((_req, res, next) => {
'Content-Security-Policy',
[
"default-src 'self'",
- "script-src 'self' 'unsafe-inline'",
+ "script-src 'self' 'unsafe-inline' https://www.truthforlife.org https://ajax.googleapis.com",
"style-src 'self' 'unsafe-inline' https://fonts.googleapis.com",
"font-src 'self' https://fonts.gstatic.com",
"img-src 'self' data: https:",
diff --git a/src/App.tsx b/src/App.tsx
index aa12fd3..a4a7fce 100644
--- a/src/App.tsx
+++ b/src/App.tsx
@@ -6,7 +6,7 @@ import QASection from './components/QASection'
import ContactForm from './components/ContactForm'
import { ColossiansStudyIndexPage, ColossiansStudyNotesPage, ColossiansStudySectionPage, StudyLandingPage, StudySignupPage, StudyAccountPage, StudyCommunityPage, StudyQuizPage } from './colossiansStudy'
import { SpotifyIcon } from './icons'
-import type { SiteContent, ArchivedSeries, StudyProgram } from './content'
+import type { SiteContent, StudyProgram } from './content'
import { DEFAULTS } from './content'
import { usePageMeta } from './hooks/usePageMeta'
import { useGlobalSearch } from './hooks/useGlobalSearch'
@@ -253,18 +253,10 @@ function buildCustomResourceDownloadId(id: string) {
return `custom:${id}`
}
-function buildArchivedResourceDownloadId(seriesId: string, linkId: string) {
- return `archived:${seriesId}:${linkId}`
-}
-
function buildCustomDownloadPageId(id: string) {
return `custom--${id}`
}
-function buildArchivedDownloadPageId(seriesId: string, linkId: string) {
- return `archived--${seriesId}--${linkId}`
-}
-
interface DownloadPageResource {
id: string
label: string
@@ -298,25 +290,6 @@ function resolveDownloadPageResource(content: SiteContent, pageId: string | unde
}
}
- if (pageId.startsWith('archived--')) {
- const [, seriesId, linkId] = pageId.split('--')
- const series = (content.archivedSeries ?? []).find(item => item.id === seriesId)
- const link = (series?.resourceLinks ?? []).find(item => item.id === linkId)
- if (!series || !link) return null
-
- return {
- id: pageId,
- label: link.label || series.title || 'Download Resource',
- imageUrl: series.imageUrl,
- summary: link.description || series.description || 'Fill out the form below to access this download from a previous study.',
- tags: [],
- buttonText: `Download ${link.label || series.title || 'Resource'}`,
- resourceId: buildArchivedResourceDownloadId(series.id, link.id),
- amazonUrl: link.amazonUrl,
- amazonLabel: link.amazonLabel,
- }
- }
-
return null
}
@@ -368,23 +341,6 @@ function AnalyticsConsentBanner({ content }: { content: SiteContent }) {
)
}
-interface Episode {
- title: string
- pubDate: string
- link: string
- description: string
- duration: string
- episode: string
-}
-
-function formatPubDate(raw: string): string {
- try {
- return new Date(raw).toLocaleDateString('en-US', { year: 'numeric', month: 'short', day: 'numeric' })
- } catch {
- return raw
- }
-}
-
function SiteSearchBar({ content }: { content: SiteContent }) {
const { query, setQuery, results } = useGlobalSearch(content)
return (
@@ -730,53 +686,26 @@ function PodcastHighlightsSection({ content }: { content: SiteContent }) {
function DownloadLibrarySection({ content }: { content: SiteContent }) {
const resources = (content.customLinks ?? []).filter(link => link.placement === 'resources')
- const archivedWithResources = (content.archivedSeries ?? []).filter(series => (series.resourceLinks ?? []).length > 0)
const [activeTag, setActiveTag] = useState(null)
- const [activeType, setActiveType] = useState<'all' | 'resource' | 'series'>('all')
- if (resources.length === 0 && archivedWithResources.length === 0) return null
+ if (resources.length === 0) return null
const allTags = Array.from(new Set(resources.flatMap(r => r.tags ?? []))).filter(Boolean)
- // Filter resources
const filteredResources = resources.filter(r => {
- const tagOk = !activeTag || (r.tags ?? []).includes(activeTag)
- const typeOk = activeType === 'all' || activeType === 'resource'
- return tagOk && typeOk
+ return !activeTag || (r.tags ?? []).includes(activeTag)
})
- // Filter archived series
- const filteredArchived = archivedWithResources.filter(() => {
- return activeType === 'all' || activeType === 'series'
- })
-
- const hasTypeFilter = resources.length > 0 && archivedWithResources.length > 0
-
return (
Download Library
-
Guides, worksheets, and past study downloads.
+
Guides, worksheets, and study downloads.
- {/* Type + tag filter bar */}
-
- {hasTypeFilter && (
-
0 ? '0.5rem' : 0 }}>
- {(['all', 'resource', 'series'] as const).map(t => (
-
- ))}
-
- )}
- {allTags.length > 0 && (activeType === 'all' || activeType === 'resource') && (
+ {allTags.length > 0 && (
+
)}
- )}
-
+
+ )}
- {filteredResources.length > 0 && (
+ {filteredResources.length > 0 ? (
{filteredResources.map(resource => (
@@ -830,86 +759,18 @@ function DownloadLibrarySection({ content }: { content: SiteContent }) {
))}
- )}
-
- {activeTag !== null && filteredResources.length === 0 && activeType !== 'series' && (
+ ) : (
No downloads tagged "{activeTag}".
)}
-
- {filteredArchived.length > 0 && (
-
-
-
Previous Studies
-
Downloads from earlier series.
-
- {filteredArchived.map(series => (
-
-
-
{series.title || 'Archived Study'}
- {series.description &&
{series.description}
}
-
-
- {(series.resourceLinks ?? []).map(link => (
-
-
- {series.imageUrl && (
-

- )}
-
-
{link.label || series.title || 'Download Resource'}
- {link.description &&
{link.description}
}
-
Open download page →
-
-
-
- ))}
-
-
- ))}
-
- )}
)
}
-function useEpisodesForSeries(seriesId: string | null, archivedSeries: ArchivedSeries[]) {
- const [episodes, setEpisodes] = useState([])
- const [loading, setLoading] = useState(false)
-
- useEffect(() => {
- if (!seriesId) return
- const series = archivedSeries.find(s => s.id === seriesId)
- if (!series?.episodeRange) return
-
- setLoading(true)
- fetch('/api/episodes/all')
- .then(r => r.ok ? r.json() : Promise.reject())
- .then((data: { episodes?: Episode[] }) => {
- const { from, to } = series.episodeRange!
- const filtered = (data.episodes ?? []).filter(ep => {
- const num = parseInt(ep.episode, 10)
- return !isNaN(num) && num >= from && num <= to
- })
- setEpisodes(filtered)
- })
- .catch(() => {})
- .finally(() => setLoading(false))
- }, [seriesId])
-
- return { episodes, loading }
-}
-
function DownloadDetailPage({ content }: { content: SiteContent }) {
const { id } = useParams<{ id: string }>()
const resource = resolveDownloadPageResource(content, id)
- // Resolve related series for archived resources
- const archivedSeriesId = id?.startsWith('archived--') ? id.split('--')[1] : null
- const relatedSeries = archivedSeriesId ? (content.archivedSeries ?? []).find(s => s.id === archivedSeriesId) : null
- const { episodes: relatedEpisodes, loading: epsLoading } = useEpisodesForSeries(archivedSeriesId, content.archivedSeries ?? [])
- const spotifyUrl = content.platformSpotifyUrl || '/episodes'
-
if (!resource) {
return (
@@ -953,39 +814,6 @@ function DownloadDetailPage({ content }: { content: SiteContent }) {
Back to Downloads
- {relatedSeries && (relatedEpisodes.length > 0 || epsLoading) && (
-
-
- Episodes from {relatedSeries.title}
-
- {epsLoading ? (
-
Loading episodes…
- ) : (
-
- )}
-
- )}
)
@@ -1140,28 +968,23 @@ function HomepageNewsletterSection() {
type TflWidgetType = 'audiodevo' | 'abdevotional' | 'resourcead'
function TruthForLifeWidget({ type = 'audiodevo' }: { type?: TflWidgetType }) {
- const iframeHeight = type === 'audiodevo' ? '420' : type === 'resourcead' ? '270' : '800'
const heading = type === 'resourcead' ? 'Recommended Resources' : 'Daily Devotional'
+ const containerId = 'tfl-syndicate-container'
- const srcdoc = `
-
-
-
-
-
-
-
-
-
-`
+ useEffect(() => {
+ const container = document.getElementById(containerId)
+ if (!container) return
+ container.innerHTML = ''
+
+ const existing = document.getElementById('syn_script')
+ if (existing) existing.remove()
+
+ const script = document.createElement('script')
+ script.id = 'syn_script'
+ script.src = `https://www.truthforlife.org/static/js/responsive/lib/syndicate.js?type=${type}&id=458890`
+ script.type = 'text/javascript'
+ container.parentElement?.insertBefore(script, container)
+ }, [type, containerId])
return (
@@ -1176,13 +999,9 @@ function TruthForLifeWidget({ type = 'audiodevo' }: { type?: TflWidgetType }) {
-
@@ -2233,7 +2052,7 @@ function PreviewPage() {
if (previewView === 'start-here') return
if (previewView === 'about') return
if (previewView === 'contact') return
- if (previewView === 'current-series' || previewView === 'archived-series' || previewView === 'episode-highlights') {
+ if (previewView === 'current-series' || previewView === 'episode-highlights') {
return
}
return
@@ -2296,7 +2115,6 @@ export default function App() {
'#qa': '/questions',
'#contact': '/contact',
'#series': '/resources',
- '#archives': '/resources',
'#resources': '/resources',
'#start-here': '/start-here',
}