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:
+30
-212
@@ -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',
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user