- Go 50.8%
- JavaScript 27.1%
- HTML 7.6%
- CSS 6.8%
- Shell 5%
- Other 2.6%
| Filename | Latest commit message | Latest commit date |
|---|---|---|
| .claude/skills | ||
| .forgejo | ||
| ansible | ||
| assets | ||
| cmd | ||
| config | ||
| content | ||
| data | ||
| dev-content | ||
| docs | ||
| i18n | ||
| internal | ||
| layouts | ||
| scripts | ||
| static | ||
| systemd | ||
| tests | ||
| tools | ||
| .env.example | ||
| .gitignore | ||
| .golangci.yaml | ||
| .stylelintrc.json | ||
| CLAUDE.md | ||
| eslint.config.js | ||
| go.mod | ||
| go.sum | ||
| hugo.prod.toml | ||
| hugo.toml | ||
| package-lock.json | ||
| package.json | ||
| README.md | ||
Coach Site
A fast, self-hosted website for coaching professionals. Hugo renders all public pages as static HTML; a small Go API handles forms, the deploy webhook, and the browser-based CMS.
Why This Over Wix/GoDaddy/WordPress?
Performance — Static files served directly by nginx. No database on the request path. Page loads under 100 ms. CSS and JS are fingerprinted and cached immutably for a year.
Simplicity — Browser-based CMS at /admin/. Edit pages and blog posts like a document, no coding required. Changes push to git and auto-deploy via webhook.
Security — No plugin surface (unlike WordPress). No shared hosting. Your server, your data.
Cost — One-time setup, then $5–10/month for a VPS. No platform subscription.
Architecture
Browser → nginx (443) → public/ Hugo-generated HTML + fingerprinted assets
↘ /admin/ bespoke admin panel — Authelia + tailnet gated
↘ /api/* → :8080 Go API: forms, CMS, deploy webhook
CI (Forgejo Actions) → /api/deploy-artifact (stages binaries) → /api/deploy-hook (triggers)
→ deploy.sh → hugo build + atomic swap + deployswap binary swap
Local Development
# Copy and edit environment variables
cp .env.example .env
# Start Hugo dev server + Go API with hot-reload
go run ./cmd/dev
# Hugo: http://localhost:1313
# CMS: http://localhost:1313/admin/
# API: http://localhost:8081
go run ./cmd/dev starts hugo server --buildDrafts --buildFuture and, via air -c tools/air.toml, the Go API with hot-reload. The Go binary is output to bin/coach-api. The dev API runs on :8081 (set in .env); production uses :8080. Pass --no-air to run the API via plain go run instead (no hot-reload).
cmd/dev is a Go program, not a shell script, so it runs the same way on macOS, Linux, and Windows — no bash/WSL required.
Useful commands
# Hugo only
hugo server --buildDrafts --buildFuture
# Go: build
go build -o bin/coach-api ./cmd/api
# Go: run tests
go test ./cmd/... ./internal/... ./tests/...
# Task runner: lists build, test, cover, lint, ... (replaces the Makefile)
go run ./cmd/tasks
# Go: single test (go finds it in whichever package defines it)
go test ./internal/handlers/ -run TestContactFormValidation
# Hugo: production build
hugo --config hugo.toml,hugo.prod.toml --minify --gc --cleanDestinationDir
# Export leads to CSV
go run ./cmd/report -db tmp/leads.db -output leads_export.csv
Production Deployment
See docs/DEPLOYMENT.md for the full Debian 12 setup guide.
Quick summary: run ansible/staging-deploy.yml against a fresh Debian 12 box — it invokes
the pinned app_base role (nginx/certbot install, the coachsite system user +
directories, TLS cert) followed by this repo's own ansible/roles/coaching_site role
(templates the nginx config — Authelia + tailnet aware, installs the systemd units, clones
the repo, and installs the git-publish deploy key). Prod has no Go toolchain — see
docs/DEPLOYMENT.md for how CI-built binaries
reach the server instead.
After initial setup, pushing a v*.*.* tag triggers CI (.forgejo/workflows/cd.yml), which
builds coach-api/precompress/deployswap, uploads them via /api/deploy-artifact, then
calls /api/deploy-hook to trigger deploy.sh on the server.
What deploy.sh does
git fetch/git reset --hardto the deployed refhugo --config hugo.toml,hugo.prod.toml --minify— merges the production baseURL override and builds into a staging dir with fingerprinted CSS/JS- Pre-compresses everything with gzip (and brotli if available) for
gzip_static on/brotli_static on - Atomically swaps the staging dir into the nginx webroot
- Swaps in each CI-staged binary via
cmd/deployswaponly if its checksum differs from what's already live — never a server-sidego build - Restarts
coach-api, pollsGET /readyz, and rolls back automatically (deployswap -rollback) if it doesn't come up healthy sudo nginx -t && sudo systemctl reload nginx
Brotli + HTTP/3
Both are wired up in the deployed nginx config, templated from
ansible/roles/coaching_site/templates/site.conf.j2.
Brotli — verify the module is installed before deploying:
nginx -V 2>&1 | grep brotli
# If missing on Debian: apt install libnginx-mod-brotli
HTTP/3 (QUIC) — requires nginx-quic or nginx mainline ≥ 1.25 built with QUIC support, and UDP port 443 open in the firewall:
nginx -V 2>&1 | grep quic # confirm QUIC support
ufw allow 443/udp # open UDP for QUIC
sudo nginx -t && sudo systemctl reload nginx
Browsers will upgrade to HTTP/3 automatically via the Alt-Svc response header.
Adding a New API Endpoint
- Write a handler in
internal/handlers/ - Register it in
registerRoutes()incmd/api/main.go - Add an OPTIONS handler alongside POST/GET if the endpoint needs CORS
Managing Content
Open http://localhost:1313/admin/ in your browser. The bespoke admin panel (not a
third-party CMS) lets you create and edit blog posts, pages, and site settings without
touching code — structured fields render as schema-driven GUI forms, not raw YAML. Save
Draft writes to disk only; Save & Publish additionally commits + pushes to git and rebuilds
the live site.
Blog posts live at content/{lang}/concepts/* (page-bundle directories). Both English and French versions must exist — Hugo does not auto-generate French from English.