Add a newsletter archive so past issues can be shared and found; v2.3.0
Publish Container / build (push) Successful in 20m51s
Publish Container / build (push) Successful in 20m51s
Web-hosted copies of episode newsletters, published from Resend broadcasts tagged with the "New Episode" topic. Gives Facebook (and anyone else) a proper link to share instead of raw HTML email, and makes issues findable via site search and Google. - Admin "Newsletter Archive" tab lists sent Resend broadcasts, flags ones tagged "New Episode", and publishes them with one click (pulls the HTML straight from Resend's API) - Public /newsletter archive index and /newsletter/:slug issue pages, server-rendered with per-issue Open Graph tags for social previews - Unsubscribe merge-tag stripped from the web copy; archive + subscribe links appended - Issues added to sitemap.xml and the on-site search index Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
This commit is contained in:
1 parent
60b23b0fba
commit
fe4c09f8a3
11 files changed
+516
-4
No files matched your search
@@ -2,7 +2,7 @@ import { useMemo, useState, useEffect, useRef } from 'react'
|
||||
import Fuse from 'fuse.js'
|
||||
import type { SiteContent } from '../content'
|
||||
|
||||
export type SearchResultType = 'episode' | 'question' | 'resource' | 'series'
|
||||
export type SearchResultType = 'episode' | 'question' | 'resource' | 'series' | 'newsletter'
|
||||
|
||||
export interface SearchResult {
|
||||
id: string
|
||||
@@ -37,6 +37,7 @@ export function useGlobalSearch(content: SiteContent) {
|
||||
const [questions, setQuestions] = useState<PublicQuestion[]>([])
|
||||
const [episodeItems, setEpisodeItems] = useState<SearchResult[]>([])
|
||||
const [nuggetsItems, setNuggetsItems] = useState<SearchResult[]>([])
|
||||
const [newsletterItems, setNewsletterItems] = useState<SearchResult[]>([])
|
||||
const [scriptResults, setScriptResults] = useState<SearchResult[]>([])
|
||||
const scriptDebounceRef = useRef<ReturnType<typeof setTimeout> | null>(null)
|
||||
|
||||
@@ -95,6 +96,24 @@ export function useGlobalSearch(content: SiteContent) {
|
||||
.catch(() => {})
|
||||
}, [])
|
||||
|
||||
// Fetch published newsletter issues once on mount
|
||||
useEffect(() => {
|
||||
fetch('/api/newsletter/issues')
|
||||
.then(r => r.ok ? r.json() : Promise.reject())
|
||||
.then((data: { issues?: Array<{ id: string; slug: string; title: string; description?: string; sentAt?: string | null }> }) => {
|
||||
const items: SearchResult[] = (data.issues ?? []).map(issue => ({
|
||||
id: `newsletter-${issue.id}`,
|
||||
type: 'newsletter' as const,
|
||||
title: issue.title,
|
||||
subtitle: 'Newsletter',
|
||||
description: issue.description,
|
||||
href: `/newsletter/${issue.slug}`,
|
||||
}))
|
||||
setNewsletterItems(items)
|
||||
})
|
||||
.catch(() => {})
|
||||
}, [])
|
||||
|
||||
// Season number → book/study title, so script results can show "Romans" instead of "Season 3"
|
||||
const seasonTitleMap = useMemo(() => {
|
||||
const map = new Map<string, string>()
|
||||
@@ -164,7 +183,7 @@ export function useGlobalSearch(content: SiteContent) {
|
||||
return results
|
||||
}, [content, questions])
|
||||
|
||||
const allFuseItems = useMemo(() => [...episodeItems, ...nuggetsItems, ...staticItems], [episodeItems, nuggetsItems, staticItems])
|
||||
const allFuseItems = useMemo(() => [...episodeItems, ...nuggetsItems, ...newsletterItems, ...staticItems], [episodeItems, nuggetsItems, newsletterItems, staticItems])
|
||||
|
||||
const fuse = useMemo(() => new Fuse(allFuseItems, {
|
||||
keys: [
|
||||
|
||||
Reference in new issue
Block a user