No description
  • Go 50.8%
  • JavaScript 27.1%
  • HTML 7.6%
  • CSS 6.8%
  • Shell 5%
  • Other 2.6%
Find a file
Repository files (latest commit first)
Filename Latest commit message Latest commit date
2026-10-19 12:25:58 -04:00
.claude/skills build: replace the Makefile with a Go task runner 2026-10-18 14:54:24 -04:00
.forgejo Add secret verification to CI workflow 2026-10-19 12:25:58 -04:00
ansible build: replace the Makefile with a Go task runner 2026-10-18 14:54:24 -04:00
assets feat: article reading experience (contents, resume, end block, topic tabs) 2026-10-15 10:12:11 -04:00
cmd build: replace the Makefile with a Go task runner 2026-10-18 14:54:24 -04:00
config feat: layered design tokens via optional tokens.client.css 2026-10-14 20:01:16 -04:00
content content: disclose the site_lang cookie by name in the privacy policy 2026-10-15 23:38:49 -04:00
data feat: per-deployment feature flags (booking, newsletter, blog) 2026-10-13 19:16:32 -04:00
dev-content build: replace the Makefile with a Go task runner 2026-10-18 14:54:24 -04:00
docs build: replace the Makefile with a Go task runner 2026-10-18 14:54:24 -04:00
i18n feat: article reading experience (contents, resume, end block, topic tabs) 2026-10-15 10:12:11 -04:00
internal test: cover the auth, CSRF, deploy-hook, admin and routing gaps 2026-10-18 14:48:00 -04:00
layouts feat: article reading experience (contents, resume, end block, topic tabs) 2026-10-15 10:12:11 -04:00
scripts build: replace the Makefile with a Go task runner 2026-10-18 14:54:24 -04:00
static ci: unblock npm audit and ESLint steps 2026-10-17 20:01:12 -04:00
systemd perf: cap coach-api/demo-session memory, fix egress-filter drift, tune nginx caching 2026-09-05 20:08:42 -04:00
tests build: replace the Makefile with a Go task runner 2026-10-18 14:54:24 -04:00
tools ci: fix golangci-lint findings and audit every severity 2026-10-18 13:14:11 -04:00
.env.example feat: migrate outbound email from Brevo's HTTP API to SMTP via the infra Postfix/Zedmail relay 2026-09-17 22:41:10 -04:00
.gitignore build: replace the Makefile with a Go task runner 2026-10-18 14:54:24 -04:00
.golangci.yaml refactor: Go module path, tests next to their code, stricter lint 2026-10-18 14:48:00 -04:00
.stylelintrc.json build: infra, deploy automation, and tooling config 2026-07-16 22:15:06 -04:00
CLAUDE.md build: replace the Makefile with a Go task runner 2026-10-18 14:54:24 -04:00
eslint.config.js ci: unblock npm audit and ESLint steps 2026-10-17 20:01:12 -04:00
go.mod refactor: Go module path, tests next to their code, stricter lint 2026-10-18 14:48:00 -04:00
go.sum perf: minify js/admin/*.js and css/admin.css in precompress 2026-08-31 13:10:14 -04:00
hugo.prod.toml fix: standardize domain placeholder and make it the single source of truth 2026-07-17 15:04:29 -04:00
hugo.toml feat: make the default content language configurable 2026-10-17 20:01:12 -04:00
package-lock.json ci: unblock npm audit and ESLint steps 2026-10-17 20:01:12 -04:00
package.json build: infra, deploy automation, and tooling config 2026-07-16 22:15:06 -04:00
README.md build: replace the Makefile with a Go task runner 2026-10-18 14:54:24 -04:00

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

  1. git fetch/git reset --hard to the deployed ref
  2. hugo --config hugo.toml,hugo.prod.toml --minify — merges the production baseURL override and builds into a staging dir with fingerprinted CSS/JS
  3. Pre-compresses everything with gzip (and brotli if available) for gzip_static on / brotli_static on
  4. Atomically swaps the staging dir into the nginx webroot
  5. Swaps in each CI-staged binary via cmd/deployswap only if its checksum differs from what's already live — never a server-side go build
  6. Restarts coach-api, polls GET /readyz, and rolls back automatically (deployswap -rollback) if it doesn't come up healthy
  7. 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

  1. Write a handler in internal/handlers/
  2. Register it in registerRoutes() in cmd/api/main.go
  3. 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.