- Admin can set birthday (month/day) per student in Study Users panel - Summer birthdays (Jun–Aug) prompt admin to set an alternate school-year date - 🎂 badge on student card header during birthday week - Dismissible happy birthday banner shown to student on their study hub - Admin receives email notification on the celebration day, once per year 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.