Headless CMS replacement for The Triangle.
server/Go APIfrontend/React dashboardscripts/setup and data helpers
The observability stack (Prometheus, Loki, Promtail, Alertmanager, blackbox,
Grafana dashboards) lives in
triangle-infrastructure.
The backend still exposes GET /metrics; scrape it directly in local dev.
API reference and data models: https://github.com/DrexelTriangle/triangle-cms/wiki
Python 3.10+, Go 1.24+, Node.js 20+, Docker with Compose, Git.
Clone into a triangle/ parent directory; the setup script clones
wordpress-etl and Scalene as siblings and installs dependencies for all
three.
mkdir triangle && cd triangle
git clone https://github.com/DrexelTriangle/triangle-cms.git
cd triangle-cms
python ./scripts/first_time_setup.pyFlags: --target-dir for a custom location, --pull to update already-cloned
repos, --skip-embeddings to skip sentence-transformers in wordpress-etl.
-
Create
.envand set strong values:cp .env.example .env
-
Generate ETL SQL (add
--generate-embeddingsfor semantic search):cd ../wordpress-etl && .venv/bin/python main.py
On Windows,
.venv/Scripts/python.exe main.py. -
Copy it into the CMS bootstrap files:
cd ../triangle-cms && python ./scripts/generate_wordpress_sql.py
-
Start the stack. Path A runs everything in Docker:
python ./scripts/setup_containers.py
Path B runs MariaDB in Docker and the backend locally:
docker compose up -d mariadb cd server && go run ./main.go
Compose services resolve each other by service name (
mariadb). The dev stack no longer runs Loki, Promtail or Grafana; their configs moved totriangle-infrastructure. Usedocker compose logs. -
Verify, then start the frontends:
curl -k https://localhost:8080/v1/articles/christmas cd frontend && npm run dev -- --port 5173 # dashboard on :5173 cd ../Scalene && npm run dev -- --port 4321 # public site on :4321
Note
Scalene only reads a local CMS on its CMS-Testing branch
(git switch CMS-Testing). Otherwise it hits production WordPress.
Reset the database and logs:
python ./scripts/setup_containers.py --reset-dataRun the backend locally against the Docker database: docker compose stop cms,
then create server/.env:
DB_NAME=triangle
DB_USER=triangle_user
DB_PASSWORD=triangle_password
DB_HOST=127.0.0.1
DB_PORT=3306
ACTIVITY_DB_PATH=./data/activityActivity events go to BadgerDB at server/data/activity locally, and to the
cms_activity_data volume under Compose.
Runs the ETL, regenerates the seed SQL, recreates the database, verifies, and restarts the CMS:
python ./scripts/reseed_from_etl.py
python ./scripts/reseed_from_etl.py --skip-etl # reuse existing ETL output
python ./scripts/reseed_from_etl.py --no-embeddings # skip the slow model step
python ./scripts/reseed_from_etl.py --yes # no confirmation promptThis is destructive. The seed files are mounted into
docker-entrypoint-initdb.d, which MariaDB replays only on first init, so a
real reseed deletes the mariadb_data volume. That takes cms_users,
cms_sessions, cms_settings, polls and the media catalogue with it, not just
imported content. The script prints the row counts at stake and makes you type
reseed; it refuses to run without a TTY unless --yes is passed.
--no-embeddings writes a placeholder that drops article_embeddings, so
related articles on /v1/articles/{slug} come back empty until you re-run with
embeddings.
For rebuilding production data from a fresh WordPress export, see
docs/ETL-REBUILD.md.
python ./scripts/generate_wordpress_sql.py [source_sql_dir] [output_dir]
WP_ETL_SQL_DIR=... WP_ETL_OUT_DIR=... python ./scripts/generate_wordpress_sql.pyWrites 01-authors.sql, 02-articles.sql, 03-articles-authors.sql,
04-seo.sql, 05-article-embeddings.sql, 06-taxonomy.sql,
07-poll-counts.sql, 08-comments.sql. Embeddings and comments fall back to
placeholders when the ETL did not produce them.
The interactive UI is at https://localhost:8080/swagger/index.html while the
server runs. A read-only copy is published to
https://drexeltriangle.github.io/triangle-cms/ from the server/docs/swagger.json
committed on main, so it is only as current as the last committed swag init,
and "Try it out" is disabled there.
The Docker build regenerates the docs. Running locally (Path B), regenerate them after changing handler annotations:
cd server && swag init --parseDependency --parseInternalDocument an endpoint with swaggo annotations above its handler in
server/internal/handlers/handlers.go:
// @Summary My endpoint
// @Tags mytag
// @Produce json
// @Success 200 {object} map[string]string
// @Router /v1/my-endpoint [get]
func MyHandler(...) {Tag order in the UI comes from the @tag.name lines in main.go.
cd server
go test ./...
go test -coverprofile=cover.out ./...