Add Verse By Verse: Nuggets sub-show with editable hosts and RSS feed
Publish Container / docker (push) Successful in 14m16s
Publish Container / docker (push) Successful in 14m16s
New /nuggets page for the companion show, linked from the Episodes nav dropdown. Hosts (name, role, bio, photo) show in a left column with the episode list on the right — latest episode featured, rest below. Episodes are pulled from an admin-configurable RSS feed URL (empty by default, shows "coming soon" until set) rather than hardcoded like the main show's feed, since the show doesn't have one yet. Everything is editable in /admin under a new Nuggets section: Show Settings (title, tagline, description, podcast art, RSS/platform URLs) and Hosts (add freely; removing a host archives them instead of deleting — kept on record and restorable, never lost). Extracted the RSS item-parsing logic shared with the main show's feed (server/routes/episodes.js) into server/rss-helpers.js so both feeds use the same parser instead of duplicating it. Also fixes the local Vite dev server not proxying /uploads/* to the backend — it was silently serving the SPA shell (200, text/html) instead of the actual file, so any admin-uploaded image (podcast art, host photos, etc.) appeared broken in local dev only. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_016Yvx4rPVy2HWe7ubuk3X3J
This commit is contained in:
1 parent
335a9cbe31
commit
31e79ad542
11 files changed
+573
-41
No files matched your search
+144
-1
@@ -4,7 +4,7 @@ import { arrayMove, SortableContext, verticalListSortingStrategy, useSortable }
|
||||
import { CSS } from '@dnd-kit/utilities'
|
||||
import type { ChangeEvent } from 'react'
|
||||
import { Link } from 'react-router-dom'
|
||||
import type { SiteContent, CustomLink, CustomBlock, ColossiansStudySection, StudyProgram, RedirectRule, PodcastFeaturedLink, SeoSettings, LegalSettings } from './content'
|
||||
import type { SiteContent, CustomLink, CustomBlock, ColossiansStudySection, StudyProgram, RedirectRule, PodcastFeaturedLink, SeoSettings, LegalSettings, PodcastHost } from './content'
|
||||
import { DEFAULTS } from './content'
|
||||
import { AnalyticsPanel } from './components/AnalyticsPanel'
|
||||
import { AdminCollapsibleCard } from './components/AdminCollapsibleCard'
|
||||
@@ -666,6 +666,7 @@ type AdminView =
|
||||
| 'about:show-copy' | 'about:bio' | 'about:testimonials'
|
||||
| 'contact:profile' | 'contact:copy'
|
||||
| 'podcast:current-series' | 'podcast:highlights' | 'podcast:finished-books' | 'podcast:scripts' | 'podcast:rss'
|
||||
| 'nuggets:settings' | 'nuggets:hosts'
|
||||
| 'downloads' | 'custom-links' | 'content-blocks' | 'books' | 'recommended-resources'
|
||||
| 'questions' | 'study-comments' | 'analytics' | 'assets' | 'colossians-study' | 'qr-codes'
|
||||
| 'subscribers' | 'study-users' | 'email-templates' | 'broadcast'
|
||||
@@ -717,6 +718,10 @@ const ADMIN_NAV: NavGroup[] = [
|
||||
{ kind: 'leaf', view: 'podcast:scripts', label: 'Episode Scripts' },
|
||||
{ kind: 'leaf', view: 'podcast:rss', label: 'RSS Feed' },
|
||||
]},
|
||||
{ kind: 'parent', id: 'nuggets', label: 'Nuggets', children: [
|
||||
{ kind: 'leaf', view: 'nuggets:settings', label: 'Show Settings' },
|
||||
{ kind: 'leaf', view: 'nuggets:hosts', label: 'Hosts' },
|
||||
]},
|
||||
{ kind: 'leaf', view: 'colossians-study', label: 'Studies' },
|
||||
{ kind: 'leaf', view: 'downloads', label: 'Resources' },
|
||||
{ kind: 'leaf', view: 'custom-links', label: 'Custom Links' },
|
||||
@@ -4574,6 +4579,144 @@ async function handleAssetUpload(event: ChangeEvent<HTMLInputElement>) {
|
||||
)
|
||||
})()}
|
||||
|
||||
{/* NUGGETS — SHOW SETTINGS */}
|
||||
{adminView === 'nuggets:settings' && (() => {
|
||||
const nuggets = form.nuggets ?? DEFAULTS.nuggets
|
||||
const updateNuggets = <K extends keyof typeof nuggets>(key: K, value: (typeof nuggets)[K]) =>
|
||||
setForm(f => ({ ...f, nuggets: { ...(f.nuggets ?? DEFAULTS.nuggets), [key]: value } }))
|
||||
|
||||
return (
|
||||
<section className="admin-panel-section" aria-label="Nuggets show settings">
|
||||
<div className="admin-panel-head">
|
||||
<h2>Nuggets — Show Settings</h2>
|
||||
<p>Verse By Verse: Nuggets is a companion show. Visitors reach it at <code>/nuggets</code>.</p>
|
||||
</div>
|
||||
<div className="admin-array-fields">
|
||||
<div className="admin-field">
|
||||
<label htmlFor="nuggets-title">Show Title</label>
|
||||
<input id="nuggets-title" type="text" value={nuggets.title} onChange={e => updateNuggets('title', e.target.value)} />
|
||||
</div>
|
||||
<div className="admin-field">
|
||||
<label htmlFor="nuggets-tagline">Tagline</label>
|
||||
<input id="nuggets-tagline" type="text" placeholder="Short verse-bite teaching, one nugget at a time." value={nuggets.tagline} onChange={e => updateNuggets('tagline', e.target.value)} />
|
||||
</div>
|
||||
<div className="admin-field">
|
||||
<label htmlFor="nuggets-description">Description</label>
|
||||
<textarea id="nuggets-description" rows={4} value={nuggets.description} onChange={e => updateNuggets('description', e.target.value)} />
|
||||
</div>
|
||||
<div className="admin-field">
|
||||
<label htmlFor="nuggets-art">Podcast Art</label>
|
||||
<input id="nuggets-art" type="text" placeholder="/uploads/nuggets-art.png" value={nuggets.podcastArtUrl} onChange={e => updateNuggets('podcastArtUrl', e.target.value)} />
|
||||
{renderImageAssetSelector(nuggets.podcastArtUrl, value => updateNuggets('podcastArtUrl', value), 'nuggets-art-asset')}
|
||||
{renderImagePreview(nuggets.podcastArtUrl, `${nuggets.title} podcast art`)}
|
||||
</div>
|
||||
<div className="admin-field">
|
||||
<label htmlFor="nuggets-rss">RSS Feed URL</label>
|
||||
<input id="nuggets-rss" type="text" placeholder="https://anchor.fm/s/xxxxxxxx/podcast/rss" value={nuggets.rssFeedUrl} onChange={e => updateNuggets('rssFeedUrl', e.target.value)} />
|
||||
<p className="admin-field-help">
|
||||
{nuggets.rssFeedUrl.trim()
|
||||
? 'The /nuggets page pulls its episode list from this feed (cached 30 minutes).'
|
||||
: 'Leave blank until the show has a feed — the page will show "Episodes coming soon" until this is set.'}
|
||||
</p>
|
||||
</div>
|
||||
<div className="admin-field">
|
||||
<label htmlFor="nuggets-spotify">Spotify URL</label>
|
||||
<input id="nuggets-spotify" type="text" placeholder="https://open.spotify.com/show/..." value={nuggets.spotifyUrl} onChange={e => updateNuggets('spotifyUrl', e.target.value)} />
|
||||
</div>
|
||||
<div className="admin-field">
|
||||
<label htmlFor="nuggets-apple">Apple Podcasts URL</label>
|
||||
<input id="nuggets-apple" type="text" placeholder="https://podcasts.apple.com/..." value={nuggets.appleUrl} onChange={e => updateNuggets('appleUrl', e.target.value)} />
|
||||
</div>
|
||||
</div>
|
||||
{renderSaveStatus()}
|
||||
</section>
|
||||
)
|
||||
})()}
|
||||
|
||||
{/* NUGGETS — HOSTS */}
|
||||
{adminView === 'nuggets:hosts' && (() => {
|
||||
const hosts = form.nuggets?.hosts ?? []
|
||||
const activeHosts = hosts.filter(h => !h.archived)
|
||||
const archivedHosts = hosts.filter(h => h.archived)
|
||||
const updateHost = (id: string, patch: Partial<PodcastHost>) =>
|
||||
setForm(f => ({
|
||||
...f,
|
||||
nuggets: { ...(f.nuggets ?? DEFAULTS.nuggets), hosts: (f.nuggets?.hosts ?? []).map(h => h.id === id ? { ...h, ...patch } : h) },
|
||||
}))
|
||||
|
||||
return (
|
||||
<section className="admin-panel-section" aria-label="Nuggets hosts">
|
||||
<div className="admin-panel-head">
|
||||
<h2>Nuggets — Hosts</h2>
|
||||
<p>Hosts shown on the public Nuggets page. Removing a host archives them — they're kept on record, never deleted, and can be restored.</p>
|
||||
</div>
|
||||
{activeHosts.length === 0 && (
|
||||
<p className="admin-stats-note">No active hosts yet. Add one below.</p>
|
||||
)}
|
||||
{activeHosts.map(host => (
|
||||
<AdminCollapsibleCard key={host.id} title={host.name || 'Unnamed host'} subtitle={host.role || 'No role set'}>
|
||||
<div className="admin-array-row">
|
||||
<div className="admin-array-fields">
|
||||
<div className="admin-field">
|
||||
<label>Name</label>
|
||||
<input type="text" value={host.name} onChange={e => updateHost(host.id, { name: e.target.value })} />
|
||||
</div>
|
||||
<div className="admin-field">
|
||||
<label>Role</label>
|
||||
<input type="text" placeholder="Co-Host" value={host.role} onChange={e => updateHost(host.id, { role: e.target.value })} />
|
||||
</div>
|
||||
<div className="admin-field">
|
||||
<label>Bio</label>
|
||||
<textarea rows={3} value={host.bio} onChange={e => updateHost(host.id, { bio: e.target.value })} />
|
||||
</div>
|
||||
<div className="admin-field">
|
||||
<label>Photo</label>
|
||||
<input type="text" placeholder="/uploads/host-photo.jpg" value={host.imageUrl} onChange={e => updateHost(host.id, { imageUrl: e.target.value })} />
|
||||
{renderImageAssetSelector(host.imageUrl, value => updateHost(host.id, { imageUrl: value }), `nuggets-host-img-${host.id}`)}
|
||||
{renderImagePreview(host.imageUrl, host.name || 'Host photo')}
|
||||
</div>
|
||||
</div>
|
||||
<button type="button" className="btn-admin-remove" onClick={() => updateHost(host.id, { archived: true })}>Archive</button>
|
||||
</div>
|
||||
</AdminCollapsibleCard>
|
||||
))}
|
||||
<button
|
||||
type="button"
|
||||
className="btn-admin-add"
|
||||
onClick={() => setForm(f => ({
|
||||
...f,
|
||||
nuggets: {
|
||||
...(f.nuggets ?? DEFAULTS.nuggets),
|
||||
hosts: [...(f.nuggets?.hosts ?? []), { id: Date.now().toString(36), name: '', role: '', bio: '', imageUrl: '', archived: false }],
|
||||
},
|
||||
}))}
|
||||
>
|
||||
+ Add Host
|
||||
</button>
|
||||
|
||||
{archivedHosts.length > 0 && (
|
||||
<div style={{ marginTop: '2.5rem' }}>
|
||||
<div className="admin-panel-head">
|
||||
<h3>Archived Hosts</h3>
|
||||
<p>Hidden from the public site. Restore to bring a host back.</p>
|
||||
</div>
|
||||
{archivedHosts.map(host => (
|
||||
<div key={host.id} className="admin-array-row" style={{ alignItems: 'center' }}>
|
||||
<div className="admin-array-fields" style={{ flexDirection: 'row', alignItems: 'center', gap: '1rem' }}>
|
||||
{host.imageUrl && <img src={host.imageUrl} alt={host.name} style={{ width: 36, height: 36, borderRadius: '50%', objectFit: 'cover' }} />}
|
||||
<span>{host.name || 'Unnamed host'}</span>
|
||||
{host.role && <span style={{ color: '#7a7060', fontSize: '0.85rem' }}>{host.role}</span>}
|
||||
</div>
|
||||
<button type="button" className="btn-admin-save" onClick={() => updateHost(host.id, { archived: false })}>Restore</button>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
)}
|
||||
{renderSaveStatus()}
|
||||
</section>
|
||||
)
|
||||
})()}
|
||||
|
||||
{/* STUDY COMMENTS */}
|
||||
{adminView === 'study-comments' && (() => {
|
||||
const filtered = studyComments.filter(c => {
|
||||
|
||||
+30
@@ -9870,6 +9870,36 @@
|
||||
margin: 0 0 0.6rem;
|
||||
}
|
||||
|
||||
/* ── Nuggets (sub-show) page ─────────────────────────────────────────────── */
|
||||
.section-nuggets-intro { padding: 1rem 1.5rem 0; }
|
||||
.nuggets-intro-layout { display: flex; gap: 2rem; align-items: flex-start; flex-wrap: wrap; }
|
||||
.nuggets-podcast-art { width: 180px; height: 180px; object-fit: cover; border-radius: 12px; flex-shrink: 0; border: 1px solid rgba(201, 168, 76, 0.18); }
|
||||
.nuggets-intro-body { flex: 1; min-width: 240px; }
|
||||
.nuggets-description { color: var(--brand-muted); line-height: 1.6; margin: 0 0 1.25rem; }
|
||||
.nuggets-platform-links { display: flex; gap: 0.9rem; flex-wrap: wrap; }
|
||||
|
||||
.section-nuggets-body { padding: 2.5rem 1.5rem 4rem; }
|
||||
.nuggets-body-layout { display: grid; grid-template-columns: minmax(240px, 300px) 1fr; gap: 2.5rem; align-items: start; }
|
||||
.nuggets-body-col { min-width: 0; }
|
||||
|
||||
.nuggets-hosts-list { display: flex; flex-direction: column; gap: 1.25rem; margin-top: 1.5rem; }
|
||||
.nuggets-host-card { display: flex; gap: 1rem; align-items: flex-start; background: rgba(26, 26, 21, 0.9); border: 1px solid rgba(201, 168, 76, 0.18); border-radius: 12px; padding: 1.1rem 1.25rem; }
|
||||
.nuggets-host-card-img { width: 64px; height: 64px; object-fit: cover; border-radius: 50%; flex-shrink: 0; border: 2px solid rgba(201, 168, 76, 0.3); }
|
||||
.nuggets-host-card-name { font-family: var(--brand-font-heading); font-size: 1.1rem; color: #e8d9b5; margin: 0; }
|
||||
.nuggets-host-card-role { font-size: 0.75rem; text-transform: uppercase; letter-spacing: 0.08em; color: var(--brand-gold); opacity: 0.85; margin: 0.2rem 0 0; }
|
||||
.nuggets-host-card-bio { font-size: 0.85rem; color: var(--brand-muted); line-height: 1.5; margin: 0.5rem 0 0; }
|
||||
|
||||
.nuggets-episodes-note { color: var(--brand-muted); margin-top: 1.5rem; }
|
||||
.nuggets-latest-episode { background: rgba(26, 26, 21, 0.9); border: 1px solid rgba(201, 168, 76, 0.25); border-radius: 12px; padding: 1.5rem 1.75rem; margin-top: 1.5rem; }
|
||||
.nuggets-latest-episode-eyebrow { font-size: 0.72rem; text-transform: uppercase; letter-spacing: 0.1em; color: var(--brand-gold); margin: 0 0 0.4rem; }
|
||||
.nuggets-latest-episode-title { font-family: var(--brand-font-heading); font-size: 1.5rem; color: #e8d9b5; margin: 0 0 0.6rem; }
|
||||
.nuggets-latest-episode-desc { color: var(--brand-muted); line-height: 1.55; margin: 0 0 1rem; }
|
||||
.nuggets-episode-list { margin-top: 2rem; }
|
||||
|
||||
@media (max-width: 800px) {
|
||||
.nuggets-body-layout { grid-template-columns: 1fr; }
|
||||
}
|
||||
|
||||
.breadcrumb {
|
||||
font-size: 0.85rem;
|
||||
color: var(--brand-muted);
|
||||
|
||||
+125
-1
@@ -383,7 +383,7 @@ function SiteHeader({ content }: { content: SiteContent }) {
|
||||
const studiesDropdownRef = useRef<HTMLDivElement>(null)
|
||||
const spotifyUrl = content.platformSpotifyUrl || '/spotify'
|
||||
const location = useLocation()
|
||||
const episodesActive = location.pathname.startsWith('/episodes') || location.pathname.startsWith('/finished')
|
||||
const episodesActive = location.pathname.startsWith('/episodes') || location.pathname.startsWith('/finished') || location.pathname.startsWith('/nuggets')
|
||||
const studiesActive = location.pathname.startsWith('/study') || location.pathname === '/scripture'
|
||||
|
||||
useEffect(() => {
|
||||
@@ -427,6 +427,7 @@ function SiteHeader({ content }: { content: SiteContent }) {
|
||||
<div className="nav-dropdown-menu-inner">
|
||||
<NavLink to="/episodes" className={({ isActive }) => `nav-dropdown-item${isActive ? ' nav-dropdown-item--active' : ''}`} onClick={() => { setMenuOpen(false); setEpisodesOpen(false) }}>Current Series</NavLink>
|
||||
<NavLink to="/finished" className={({ isActive }) => `nav-dropdown-item${isActive ? ' nav-dropdown-item--active' : ''}`} onClick={() => { setMenuOpen(false); setEpisodesOpen(false) }}>Finished Books</NavLink>
|
||||
<NavLink to="/nuggets" className={({ isActive }) => `nav-dropdown-item${isActive ? ' nav-dropdown-item--active' : ''}`} onClick={() => { setMenuOpen(false); setEpisodesOpen(false) }}>Nuggets</NavLink>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
@@ -1863,6 +1864,128 @@ function EpisodesPage({ content }: { content: SiteContent }) {
|
||||
)
|
||||
}
|
||||
|
||||
interface NuggetsEpisode {
|
||||
title: string
|
||||
pubDate: string
|
||||
link: string
|
||||
audioUrl: string
|
||||
description: string
|
||||
duration: string
|
||||
episode: string
|
||||
season: string
|
||||
}
|
||||
|
||||
function NuggetsPage({ content }: { content: SiteContent }) {
|
||||
const siteTitle = content.seo?.title || DEFAULTS.seo.title
|
||||
const nuggets = content.nuggets ?? DEFAULTS.nuggets
|
||||
usePageMeta(
|
||||
`${nuggets.title} | ${siteTitle}`,
|
||||
nuggets.description || nuggets.tagline || `${nuggets.title} — a companion show from ${siteTitle}.`,
|
||||
)
|
||||
|
||||
const [episodes, setEpisodes] = useState<NuggetsEpisode[]>([])
|
||||
const [loading, setLoading] = useState(true)
|
||||
const [configured, setConfigured] = useState(true)
|
||||
|
||||
useEffect(() => {
|
||||
fetch('/api/nuggets/episodes/all')
|
||||
.then(r => r.json())
|
||||
.then((d: { episodes?: NuggetsEpisode[]; configured?: boolean }) => {
|
||||
setEpisodes(Array.isArray(d.episodes) ? d.episodes : [])
|
||||
setConfigured(d.configured !== false)
|
||||
})
|
||||
.catch(() => {})
|
||||
.finally(() => setLoading(false))
|
||||
}, [])
|
||||
|
||||
const [latest, ...rest] = episodes
|
||||
const activeHosts = (nuggets.hosts ?? []).filter(h => !h.archived)
|
||||
|
||||
return (
|
||||
<div className="site">
|
||||
<SiteHeader content={content} />
|
||||
<SiteSearchBar content={content} />
|
||||
<PageBanner eyebrow="Sub-Show" title={nuggets.title} desc={nuggets.tagline} />
|
||||
|
||||
{(nuggets.podcastArtUrl || nuggets.description || nuggets.spotifyUrl || nuggets.appleUrl) && (
|
||||
<section className="section-nuggets-intro" aria-label={`About ${nuggets.title}`}>
|
||||
<div className="section-inner nuggets-intro-layout">
|
||||
{nuggets.podcastArtUrl && <img src={nuggets.podcastArtUrl} alt={nuggets.title} className="nuggets-podcast-art" />}
|
||||
<div className="nuggets-intro-body">
|
||||
{nuggets.description && <p className="nuggets-description">{nuggets.description}</p>}
|
||||
{(nuggets.spotifyUrl || nuggets.appleUrl) && (
|
||||
<div className="nuggets-platform-links">
|
||||
{nuggets.spotifyUrl && <a href={nuggets.spotifyUrl} target="_blank" rel="noopener noreferrer" className="btn-primary">Listen on Spotify</a>}
|
||||
{nuggets.appleUrl && <a href={nuggets.appleUrl} target="_blank" rel="noopener noreferrer" className="btn-secondary">Listen on Apple Podcasts</a>}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
)}
|
||||
|
||||
<section className="section-nuggets-body" aria-label={`${nuggets.title} hosts and episodes`}>
|
||||
<div className="section-inner nuggets-body-layout">
|
||||
<div className="nuggets-body-col nuggets-body-hosts">
|
||||
{activeHosts.length > 0 && (
|
||||
<>
|
||||
<p className="eyebrow">Meet the Hosts</p>
|
||||
<div className="nuggets-hosts-list">
|
||||
{activeHosts.map(host => (
|
||||
<div key={host.id} className="nuggets-host-card">
|
||||
{host.imageUrl && <img src={host.imageUrl} alt={host.name} className="nuggets-host-card-img" loading="lazy" />}
|
||||
<h3 className="nuggets-host-card-name">{host.name}</h3>
|
||||
{host.role && <p className="nuggets-host-card-role">{host.role}</p>}
|
||||
{host.bio && <p className="nuggets-host-card-bio">{host.bio}</p>}
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
</>
|
||||
)}
|
||||
</div>
|
||||
|
||||
<div className="nuggets-body-col nuggets-body-episodes">
|
||||
<p className="eyebrow">Episodes</p>
|
||||
{loading ? (
|
||||
<p className="nuggets-episodes-note">Loading episodes…</p>
|
||||
) : !configured ? (
|
||||
<p className="nuggets-episodes-note">Episodes are coming soon — check back once the show launches.</p>
|
||||
) : episodes.length === 0 ? (
|
||||
<p className="nuggets-episodes-note">No episodes published yet.</p>
|
||||
) : (
|
||||
<>
|
||||
<div className="nuggets-latest-episode">
|
||||
<p className="nuggets-latest-episode-eyebrow">Latest Episode</p>
|
||||
<h2 className="nuggets-latest-episode-title">{latest.title}</h2>
|
||||
{latest.description && <p className="nuggets-latest-episode-desc">{latest.description}</p>}
|
||||
{latest.audioUrl && <EpisodeAudioPlayer src={latest.audioUrl} title={latest.title} size="full" />}
|
||||
</div>
|
||||
{rest.length > 0 && (
|
||||
<ol className="finished-series-playlist nuggets-episode-list">
|
||||
{rest.map((ep, i) => (
|
||||
<li key={ep.audioUrl || i} className="finished-series-episode">
|
||||
<span className="finished-series-ep-num">{ep.episode || i + 2}</span>
|
||||
<div className="finished-series-ep-body">
|
||||
<p className="finished-series-ep-title">{ep.title}</p>
|
||||
{ep.duration && <p className="finished-series-ep-meta">{ep.duration}</p>}
|
||||
{ep.audioUrl && <EpisodeAudioPlayer src={ep.audioUrl} title={ep.title} />}
|
||||
</div>
|
||||
</li>
|
||||
))}
|
||||
</ol>
|
||||
)}
|
||||
</>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<SiteFooter content={content} />
|
||||
<AnalyticsConsentBanner content={content} />
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
function BooksAndResourcesSection({ content }: { content: SiteContent }) {
|
||||
const books = (content.customLinks ?? []).filter(link => link.placement === 'books')
|
||||
const recommended = (content.customLinks ?? []).filter(link => link.placement === 'recommended')
|
||||
@@ -3001,6 +3124,7 @@ export default function App() {
|
||||
<Route path="/finished/:id" element={<FinishedSeriesPage content={content} />} />
|
||||
<Route path="/episodes" element={<EpisodesPage content={content} />} />
|
||||
<Route path="/episodes/:id" element={<EpisodeDetailPage content={content} />} />
|
||||
<Route path="/nuggets" element={<NuggetsPage content={content} />} />
|
||||
<Route path="/resources" element={<ResourcesPage content={content} />} />
|
||||
<Route path="/downloads/:id" element={<DownloadDetailPage content={content} />} />
|
||||
<Route path="/about" element={<AboutPage content={content} />} />
|
||||
|
||||
@@ -93,6 +93,28 @@ export interface FinishedBook {
|
||||
season: number
|
||||
}
|
||||
|
||||
export interface PodcastHost {
|
||||
id: string
|
||||
name: string
|
||||
role: string
|
||||
bio: string
|
||||
imageUrl: string
|
||||
// Removed hosts are archived, never deleted — kept out of public display but
|
||||
// preserved so past-episode credits and history stay intact.
|
||||
archived: boolean
|
||||
}
|
||||
|
||||
export interface NuggetsShow {
|
||||
title: string
|
||||
tagline: string
|
||||
description: string
|
||||
podcastArtUrl: string
|
||||
rssFeedUrl: string
|
||||
spotifyUrl: string
|
||||
appleUrl: string
|
||||
hosts: PodcastHost[]
|
||||
}
|
||||
|
||||
export interface SeoSettings {
|
||||
title: string
|
||||
description: string
|
||||
@@ -298,6 +320,8 @@ export interface SiteContent {
|
||||
finishedBooks: FinishedBook[]
|
||||
// ── Testimonials ──
|
||||
testimonials: Testimonial[]
|
||||
// ── Nuggets (sub-show) ──
|
||||
nuggets: NuggetsShow
|
||||
}
|
||||
|
||||
export const DEFAULTS: SiteContent = {
|
||||
@@ -525,6 +549,16 @@ export const DEFAULTS: SiteContent = {
|
||||
podcastFeaturedLinks: [],
|
||||
finishedBooks: [],
|
||||
testimonials: [],
|
||||
nuggets: {
|
||||
title: 'Verse By Verse: Nuggets',
|
||||
tagline: '',
|
||||
description: '',
|
||||
podcastArtUrl: '',
|
||||
rssFeedUrl: '',
|
||||
spotifyUrl: '',
|
||||
appleUrl: '',
|
||||
hosts: [],
|
||||
},
|
||||
episodesSeoIntro: 'Verse by Verse with Nate is an expository Bible teaching podcast where we go through Scripture one verse at a time. Each episode digs into the text carefully and practically, helping you understand what the Bible says, what it means, and how to live it out. New episodes released regularly — subscribe on Spotify or your favorite podcast platform.',
|
||||
seo: {
|
||||
title: 'Verse by Verse with Nate',
|
||||
|
||||
Reference in new issue
Block a user