# 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.gz` data export/import for server migration - **Version display** — app version and git commit shown in admin sidebar; `/api/version` endpoint --- ## Tech Stack | Layer | Technology | |---|---| | Frontend | React 19, TypeScript, Vite, React Router v7 | | Backend | Express 5, Node.js 22 | | Email | 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 `) | | `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 ```bash # 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. ```yaml volumes: - /path/to/your/data:/app/data ``` ### Useful commands ```bash # 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 1. Navigate to `/admin` 2. Enter the admin password (hashed value in `ADMIN_PASSWORD_HASH`) 3. Complete TOTP 2FA, or use an email OTP if no TOTP app is enrolled 4. 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.