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 <noreply@anthropic.com>
This commit is contained in:
nmemmert
2026-06-30 09:26:00 -04:00
parent 2169739910
commit a8f6024234
2 changed files with 31 additions and 213 deletions
+30 -212
View File
@@ -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<string | null>(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 (
<section className="section-resources section-download-library" aria-label="Download library">
<div className="section-inner">
<div className="download-library-head">
<p className="eyebrow">Download Library</p>
<h2 className="section-heading">Guides, worksheets, and past study downloads.</h2>
<h2 className="section-heading">Guides, worksheets, and study downloads.</h2>
</div>
{/* Type + tag filter bar */}
<div className="download-filter-bar">
{hasTypeFilter && (
<div className="download-tag-filter" style={{ marginBottom: allTags.length > 0 ? '0.5rem' : 0 }}>
{(['all', 'resource', 'series'] as const).map(t => (
<button
key={t}
type="button"
className={`download-tag-chip${activeType === t ? ' download-tag-chip--active' : ''}`}
onClick={() => setActiveType(t)}
>
{t === 'all' ? 'All types' : t === 'resource' ? 'Study guides' : 'Past series'}
</button>
))}
</div>
)}
{allTags.length > 0 && (activeType === 'all' || activeType === 'resource') && (
{allTags.length > 0 && (
<div className="download-filter-bar">
<div className="download-tag-filter">
<button
type="button"
@@ -799,16 +728,16 @@ function DownloadLibrarySection({ content }: { content: SiteContent }) {
<button
type="button"
className="download-clear-link"
onClick={() => { setActiveTag(null); setActiveType('all') }}
onClick={() => setActiveTag(null)}
>
Clear
</button>
)}
</div>
)}
</div>
</div>
)}
{filteredResources.length > 0 && (
{filteredResources.length > 0 ? (
<div className="download-library-group">
<div className="resources-list">
{filteredResources.map(resource => (
@@ -830,86 +759,18 @@ function DownloadLibrarySection({ content }: { content: SiteContent }) {
))}
</div>
</div>
)}
{activeTag !== null && filteredResources.length === 0 && activeType !== 'series' && (
) : (
<p className="download-empty-state">No downloads tagged "{activeTag}".</p>
)}
{filteredArchived.length > 0 && (
<div className="download-library-group">
<div className="download-library-group-head">
<h3>Previous Studies</h3>
<p>Downloads from earlier series.</p>
</div>
{filteredArchived.map(series => (
<div key={series.id} className="archive-series-resources">
<div className="download-library-series-head">
<h4>{series.title || 'Archived Study'}</h4>
{series.description && <p>{series.description}</p>}
</div>
<div className="resources-list">
{(series.resourceLinks ?? []).map(link => (
<Link key={link.id} to={`/downloads/${buildArchivedDownloadPageId(series.id, link.id)}`} className="resource-download-card resource-download-card--link">
<div className="resource-download-header">
{series.imageUrl && (
<img src={series.imageUrl} alt={series.title || 'Archived study'} className="resource-link-image" loading="lazy" />
)}
<div className="resource-download-meta">
<span className="resource-link-label">{link.label || series.title || 'Download Resource'}</span>
{link.description && <p className="resource-link-description">{link.description}</p>}
<span className="resource-link-action">Open download page </span>
</div>
</div>
</Link>
))}
</div>
</div>
))}
</div>
)}
</div>
</section>
)
}
function useEpisodesForSeries(seriesId: string | null, archivedSeries: ArchivedSeries[]) {
const [episodes, setEpisodes] = useState<Episode[]>([])
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 (
<main className="thanks-page" aria-label="Download not found">
@@ -953,39 +814,6 @@ function DownloadDetailPage({ content }: { content: SiteContent }) {
<Link to="/resources" className="btn-secondary">Back to Downloads</Link>
</div>
{relatedSeries && (relatedEpisodes.length > 0 || epsLoading) && (
<div className="download-related-episodes">
<h2 className="download-related-heading">
Episodes from {relatedSeries.title}
</h2>
{epsLoading ? (
<p style={{ color: '#7a7060', fontSize: '0.9rem' }}>Loading episodes</p>
) : (
<div className="download-related-list">
{relatedEpisodes.slice(0, 10).map((ep, idx) => (
<a
key={idx}
href={ep.link || spotifyUrl}
target="_blank"
rel="noreferrer"
className="download-related-ep"
>
<div className="download-related-ep-meta">
{ep.episode && <span className="episode-number">Ep. {ep.episode}</span>}
{ep.pubDate && <span className="episode-date">{formatPubDate(ep.pubDate)}</span>}
</div>
<p className="download-related-ep-title">{ep.title}</p>
</a>
))}
{relatedEpisodes.length > 10 && (
<Link to="/episodes" className="btn-secondary" style={{ marginTop: '0.5rem', display: 'inline-block' }}>
View all {relatedEpisodes.length} episodes
</Link>
)}
</div>
)}
</div>
)}
</div>
</main>
)
@@ -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 = `<!DOCTYPE html>
<html>
<head>
<meta charset="utf-8" />
<style>
html, body { margin: 0; padding: 0; background: #ffffff; overflow: hidden; }
#tfl-syndicate-container { font-family: Georgia, serif; }
/* Hide the widget's own heading since our section already has one */
#tfl-syndicate-container h2,
#tfl-syndicate-container h3,
#tfl-syndicate-container > *:first-child > h2,
#tfl-syndicate-container > *:first-child > h3 { display: none; }
</style>
</head>
<body>
<script id="syn_script" src="https://www.truthforlife.org/static/js/responsive/lib/syndicate.js?type=${type}&id=458890" type="text/javascript"></script>
<div id="tfl-syndicate-container"></div>
</body>
</html>`
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 (
<section className="section-tfl" aria-label="Truth For Life devotional" style={type === 'resourcead' ? { padding: '2.5rem 0' } : undefined}>
@@ -1176,13 +999,9 @@ function TruthForLifeWidget({ type = 'audiodevo' }: { type?: TflWidgetType }) {
</p>
</div>
<div className="tfl-widget-wrap">
<iframe
srcDoc={srcdoc}
title="Truth For Life daily devotional"
width="100%"
height={iframeHeight}
style={{ border: 'none', display: 'block', borderRadius: '10px', maxWidth: type === 'resourcead' ? '500px' : '760px', margin: '0 auto' }}
sandbox="allow-scripts allow-same-origin allow-popups"
<div
id="tfl-syndicate-container"
style={{ maxWidth: type === 'resourcead' ? '500px' : '760px', margin: '0 auto' }}
/>
</div>
</div>
@@ -2233,7 +2052,7 @@ function PreviewPage() {
if (previewView === 'start-here') return <StartHerePage content={content} />
if (previewView === 'about') return <AboutPage content={content} />
if (previewView === 'contact') return <ContactPage content={content} />
if (previewView === 'current-series' || previewView === 'archived-series' || previewView === 'episode-highlights') {
if (previewView === 'current-series' || previewView === 'episode-highlights') {
return <EpisodesPage content={content} />
}
return <LandingPage content={content} />
@@ -2296,7 +2115,6 @@ export default function App() {
'#qa': '/questions',
'#contact': '/contact',
'#series': '/resources',
'#archives': '/resources',
'#resources': '/resources',
'#start-here': '/start-here',
}