Adds /api/bible-passage server route that proxies the Berean Standard Bible from Bolls.life per-verse, with in-memory caching. Study lesson scripture block now displays live BSB text instead of static passageText. Adds bibleBook field to StudyProgram so admins can set the book name explicitly when the study slug doesn't match (e.g. a study titled "ephesians-part-2"). Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Siteforge
A podcast and ministry website for Verse by Verse with Nate — a verse-by-verse Bible teaching podcast. Built with React + TypeScript (Vite) on the frontend and Express.js on the backend, deployed as a Docker container via GitHub Actions.
Features
- Podcast episodes — episode listing, detail pages, downloadable resources, finished series archive
- Study system — per-user enrollment in structured Bible studies with sections, notes, progress tracking, section quizzes, community posts/comments, and completion certificates
- Study accounts — signup/login, email verification, avatar upload, email change with re-verification
- Two-factor authentication — TOTP authenticator app + email OTP backup, recovery codes
- Contact form — submissions stored server-side, admin reply with templates, inbound email webhook (Resend)
- Q&A — public question submission, admin moderation, pinned answers, shareable question links
- Email integration — welcome emails, admin notifications, admin replies, study reminder emails (via Resend)
- Admin panel — content management with draft/publish workflow, episode scripts, podcast checklist, QR code management, subscriber management, asset uploads
- Analytics — page views, audio plays, download tracking, geo-based visitor stats (country/state/city), consent-based cookie opt-in
- Backup system — automatic JSON snapshots with restore, full
tar.gzdata export/import for server migration - Version display — app version and git commit shown in admin sidebar;
/api/versionendpoint
Tech Stack
| Layer | Technology |
|---|---|
| Frontend | React 19, TypeScript, Vite, React Router v7 |
| Backend | Express 5, Node.js 22 |
| Resend | |
| TOTP | otplib |
| QR Codes | qrcode |
| Rich text | TipTap |
| Charts | Chart.js + react-chartjs-2, react-simple-maps |
| Documents | docx, mammoth |
| Container | Docker (GHCR), GitHub Actions CI |
Project Structure
siteforge/
├── src/ # React frontend
│ ├── App.tsx # Root + all client routes
│ ├── App.css
│ └── ... # Page and component files
├── server/ # Express backend
│ ├── config.js # Constants, env vars, APP_VERSION, GIT_COMMIT
│ ├── data.js # In-memory state + JSON persistence
│ ├── helpers.js # Shared utilities (IP, rate limiting, etc.)
│ ├── study-helpers.js # Study email scheduling, cert generation
│ └── routes/ # Route handlers (one file per domain)
├── data/ # Seed data (JSON); live data at /app/data in container
├── server.js # Entry point — mounts all routers
├── Dockerfile
├── entrypoint.sh # Seeds /app/data on first run, never overwrites live data
├── docker-compose.yml
└── .github/workflows/docker-publish.yml
Environment Variables
| Variable | Required | Description |
|---|---|---|
SESSION_SECRET |
Yes | Secret for Express session signing |
ADMIN_PASSWORD_HASH |
Yes | bcrypt hash of the admin password |
RESEND_API_KEY |
Yes | Resend API key for sending email |
RESEND_FROM |
Yes | Sender address (e.g. Nate <nate@example.com>) |
RESEND_TO |
Yes | Address that receives contact form notifications |
RESEND_REPLY_TO |
No | Reply-to address on outbound emails |
RESEND_WEBHOOK_TOKEN |
No | Validates inbound Resend webhook requests |
INBOUND_EMAIL_SECRET |
No | Validates inbound email route requests |
RESEND_CONTACTS_API_KEY |
No | Resend Contacts API key for subscriber sync |
RESEND_SEGMENT_ID |
No | Resend segment ID for subscriber list |
COMMIT_SHA |
No | Git SHA baked in at build time by CI (shown in admin UI) |
TRUST_PROXY_HOPS |
No | Number of proxy hops to trust (default 1) — set correctly for accurate client IPs behind a reverse proxy |
ALLOW_INSECURE_COOKIES |
No | Set to true to allow session cookies over HTTP (for local/non-HTTPS access) |
TITUS_STUDY_FILE |
No | Path to the Titus study PDF (defaults to bundled file) |
TITUS_STUDY_DOWNLOAD_NAME |
No | Filename shown to users downloading the Titus PDF |
PORT |
No | HTTP port (default 4173) |
CACHE_PURGE_WEBHOOK_URL |
No | Webhook URL for one-click cache purge from Admin Operations |
DEPLOY_WEBHOOK_URL |
No | Webhook URL for one-click deploy trigger from Admin Operations |
Frontend Routes
| Path | Page |
|---|---|
/ |
Landing page |
/start-here |
Start here / intro page |
/episodes |
Episode listing |
/episodes/:id |
Episode detail |
/finished |
Finished series listing |
/finished/:id |
Finished series detail |
/resources |
Resources listing |
/downloads/:id |
Download detail |
/about |
About page |
/contact |
Contact form |
/questions |
Q&A listing |
/subscribe |
Email subscribe |
/subscribe/thanks |
Subscribe confirmation |
/study |
Study landing page |
/study/signup |
Study account signup |
/study/account |
Study account dashboard |
/study/:studySlug |
Study index |
/study/:studySlug/community |
Study community posts |
/study/:studySlug/notes |
Study notes |
/study/:studySlug/:sectionId |
Study section |
/study/:studySlug/:sectionId/quiz |
Section quiz |
/certificate/:token |
Public completion certificate |
/privacy |
Privacy policy |
/thanks |
Contact thank-you |
/admin |
Admin panel (requires password + TOTP) |
/preview |
Content draft preview |
Key API Endpoints
| Endpoint | Description |
|---|---|
GET /api/version |
Returns { version, commit } |
GET /api/content |
Public site content (CMS data) |
POST /api/contact |
Submit contact form |
POST /api/analytics/pageview |
Record page view |
POST /api/analytics/play |
Record audio play |
GET /api/episode-audio |
Episode audio proxy |
GET /api/admin-auth/status |
Admin session status + version info |
POST /api/admin-auth/login |
Admin login |
POST /api/admin-auth/totp-verify |
Complete TOTP 2FA |
POST /api/admin-auth/totp-setup-init |
Begin TOTP enrollment |
POST /api/admin-auth/totp-setup-confirm |
Confirm TOTP enrollment |
GET /api/admin-stats |
Hit/visitor analytics |
GET /api/admin-stats/backup |
Trigger stats snapshot |
GET /api/admin-backup/export |
Download full tar.gz data backup |
POST /api/admin-backup/import |
Restore from tar.gz backup |
GET /api/admin/study-users |
List study accounts |
GET /api/admin-study-certificates |
Study completion certificates |
GET /api/admin-contact-submissions |
Contact form submissions |
POST /api/admin-contact-submissions/:id/reply |
Reply to a submission |
GET /api/admin/qr-codes |
QR code management |
GET /api/admin-episode-scripts/:episodeNumber |
Episode script |
GET /api/admin-podcast-checklist |
Podcast checklist state |
GET /api/admin-subscribers |
Subscriber list |
Development
# Install dependencies
npm install
# Start frontend dev server only (Vite, http://localhost:5173)
npm run dev
# Start backend API server only (reads .env, http://localhost:4173)
npm run api
# Start both concurrently
npm run dev:full
# Production build
npm run build
# Start production server (reads .env)
npm start
Create a .env file at the project root with the required environment variables before running locally. The server uses --env-file=.env.
Docker / Deployment
Images are built and pushed to ghcr.io/nmemmert/siteforge automatically on every push to main or when a v* tag is pushed. The git commit SHA is baked into the image as COMMIT_SHA at build time and displayed in the admin sidebar.
First run
On first boot, entrypoint.sh copies seed data from the bundled data-seed/ into /app/data/ if that directory is empty. Subsequent restarts skip this step, so live data is never overwritten by an upgrade.
docker-compose.yml
A docker-compose.yml is included at the project root. Mount /app/data to a persistent volume or host path and supply all required environment variables.
volumes:
- /path/to/your/data:/app/data
Useful commands
# Pull latest image and restart
docker compose pull
docker compose up -d
# View logs
docker compose logs -f
# Stop
docker compose down
Persistent Data
All server-managed data lives under /app/data (or ./data locally):
| Path | Contents |
|---|---|
admin-content.json |
Published CMS content |
admin-content-draft.json |
Draft content (unpublished) |
hit-stats.json |
Page hit analytics |
visitor-stats.json |
Detailed visitor analytics |
contact-submissions.json |
Contact form submissions |
study-users.json |
Study account records |
study-reminders.json |
Scheduled study reminder state |
qr-codes.json |
QR code registry |
episode-scripts/ |
Per-episode script files |
backups/ |
Automatic JSON snapshot backups |
uploads/ |
Admin-uploaded assets (served at /uploads/*) |
Admin Access
- Navigate to
/admin - Enter the admin password (hashed value in
ADMIN_PASSWORD_HASH) - Complete TOTP 2FA, or use an email OTP if no TOTP app is enrolled
- On first use, enroll a TOTP authenticator app from Admin → Settings
Recovery codes are generated during TOTP setup and can be regenerated from Admin → Settings.
Admin capabilities
- Content — edit and publish site content with draft/preview workflow
- Episode scripts — write and manage per-episode scripts; export to
.docx - Podcast checklist — track per-episode production steps
- QR codes — create, manage, and track scan counts for QR codes
- Questions — moderate submitted Q&A, pin answers, reply
- Contact — view submissions, reply with templates, check email health
- Study users — view enrollments, progress, certificates, manually enroll/unenroll
- Study comments — moderate community post comments
- Subscribers — view and export subscriber list
- Assets — upload and delete hosted images
- Stats — page hits, visitor geo analytics, download stats; export CSV
- Backup — snapshot now, restore from snapshot, export/import full
tar.gz - Operations — purge cache, trigger deploy, manage SEO/sitemap/redirects
Versioning
The app version is read from package.json at startup (APP_VERSION). The git commit is baked in via COMMIT_SHA at Docker build time (set automatically by CI). Both are visible in the admin sidebar and at GET /api/version.
Bump package.json before every commit pushed to main: patch for fixes, minor for features, major for breaking changes.