Add a newsletter archive so past issues can be shared and found; v2.3.0
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:
nmemmertandClaude Sonnet 5 committed 2026-09-28 12:17:13 -04:00
1 parent 60b23b0fba
commit fe4c09f8a3
11 files changed
+516 -4

No files matched your search

+4
View File
@@ -29,6 +29,7 @@ import {
loadWebhooksFromDisk,
loadStudySessionsFromDisk,
queueStudySessionsWrite,
loadNewsletterIssuesFromDisk,
createBackupSnapshot,
refreshContentCaches,
queueHitStatsWrite,
@@ -70,6 +71,7 @@ import { startNuggetsSeafileWatcher } from './server/nuggets-seafile-watcher.js'
import { register as registerFeed } from './server/routes/feed.js'
import { register as registerQrCodes } from './server/routes/qr-codes.js'
import { register as registerBiblePassage } from './server/routes/bible-passage.js'
import { register as registerNewsletter } from './server/routes/newsletter.js'
import { register as registerPublic } from './server/routes/public.js'
const app = express()
@@ -120,6 +122,7 @@ registerNuggetsHostUpload(app)
registerFeed(app)
registerQrCodes(app)
registerBiblePassage(app)
registerNewsletter(app)
// Hit-counting middleware (must come before public routes)
app.use((req, res, next) => {
@@ -168,6 +171,7 @@ Promise.all([
loadAuditLogFromDisk(),
loadWebhooksFromDisk(),
loadStudySessionsFromDisk(),
loadNewsletterIssuesFromDisk(),
refreshContentCaches(),
])
.catch(err => {