From 449b763621f32b3faaeaf5f98bf9751624e3702e Mon Sep 17 00:00:00 2001 From: Timothy Kim Date: Thu, 9 Apr 2026 16:54:35 -0400 Subject: [PATCH] add goatcounter tracking to all sites via nginx sub_filter --- ARCHITECTURE.md | 13 +++++++++++++ CLAUDE.md | 1 + README.md | 2 ++ USECASES.md | 14 ++++++++++++-- docker/nginx/conf.d/goatcounter.inc | 3 +++ docker/nginx/conf.d/haanmind.net.conf | 1 + docker/nginx/conf.d/hantim.net.conf | 1 + docker/nginx/conf.d/hcsuzuki.music.conf | 1 + docker/nginx/conf.d/hcsuzuki.net.conf | 1 + docker/nginx/conf.d/thekims.family.conf | 1 + docker/nginx/conf.d/timothykim.net.conf | 1 + tools/app.sh | 1 + 12 files changed, 38 insertions(+), 2 deletions(-) create mode 100644 docker/nginx/conf.d/goatcounter.inc diff --git a/ARCHITECTURE.md b/ARCHITECTURE.md index 035914f..0c0cd19 100644 --- a/ARCHITECTURE.md +++ b/ARCHITECTURE.md @@ -133,6 +133,19 @@ If any secret is missing, deploy fails with an error (not a warning). - **One domain per conf file** — `deploy.sh` derives the cert name from the filename and collects all `server_name` values for that cert +## GoatCounter (analytics) + +GoatCounter runs at `goatcounter.hantim.net` for privacy-friendly web analytics. + +**Script injection:** nginx injects the GoatCounter tracking script into all +HTML responses via `sub_filter`. The shared include `conf.d/goatcounter.inc` +is added to each site's main HTTPS server block. New sites get it automatically +via `app.sh`. + +**Config:** `docker/goatcounter/compose.yaml` uses the `baethon/goatcounter` +image. Password is stored in Bitwarden (`hantim-goatcounter-password`) and +injected via the `.env.keys` mechanism. + ## URL routing - All HTTP → HTTPS redirect diff --git a/CLAUDE.md b/CLAUDE.md index 3e72c11..781f63c 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -13,6 +13,7 @@ for technical deep-dives, USECASES.md for expected behaviors. - `tools/remove-app.sh` -- Remove a static site (local files + Gitea repo) - `docker/nginx/conf.d/` -- Per-app nginx server blocks - `docker//compose.yml` -- Per-app compose files +- `docker/goatcounter/` -- GoatCounter analytics (compose.yaml, .env.keys) - `docker/garage/` -- Garage S3 object storage (Dockerfile, garage.toml, .env.keys) - `.gitea/workflows/` -- Per-app deploy workflows + provision.yml diff --git a/README.md b/README.md index e7a08f2..304facd 100644 --- a/README.md +++ b/README.md @@ -38,6 +38,7 @@ docker/ compose.yml / # Per-app compose files (e.g. example.com) compose.yml + goatcounter/ # GoatCounter analytics garage/ # Garage S3-compatible object storage compose.yml Dockerfile @@ -195,6 +196,7 @@ Project: `hantim`. Fetched via `bws` CLI on the server. | `hantim-garage-media-key-id` | S3 access key ID for media uploads | `app.sh`, `aws` CLI | | `hantim-garage-media-secret-key` | S3 secret key for media uploads | `aws` CLI | | `hantim-uptimerobot-api-key` | UptimeRobot API key for monitors | `app.sh` | +| `hantim-goatcounter-password` | GoatCounter admin password | `deploy.sh` | Machine accounts: `hantim-server` (token at `/etc/bws-token`), `hantim-ci` (reserved). diff --git a/USECASES.md b/USECASES.md index 8f46e54..2e1711b 100644 --- a/USECASES.md +++ b/USECASES.md @@ -102,7 +102,17 @@ for stateless static sites. - SSL cert issued for `.hantim.net` (no www variant) - Service ready for customization (edit compose.yml image, add `.env.keys` if needed) -## 9. Upload media files +## 9. Analytics tracking + +**Trigger:** automatic — nginx injects the GoatCounter script into all HTML responses. + +**Expected outcome:** +- Every site includes the GoatCounter tracking script (injected via `sub_filter`) +- Page views are recorded at `goatcounter.hantim.net` +- New sites provisioned with `app.sh` include tracking automatically +- GoatCounter dashboard itself is not tracked + +## 10. Upload media files **Trigger:** `aws s3 cp` or `aws s3 sync` to `s3://example.com/`, or `./media.sh push` from a site repo. @@ -111,7 +121,7 @@ for stateless static sites. - Replicated to both Garage nodes (hantim + argento) - Accessible at `https://www.example.com/media/` -## 10. Local development with media files +## 11. Local development with media files **Trigger:** `./media.sh pull` from a site repo. diff --git a/docker/nginx/conf.d/goatcounter.inc b/docker/nginx/conf.d/goatcounter.inc new file mode 100644 index 0000000..7df1ba7 --- /dev/null +++ b/docker/nginx/conf.d/goatcounter.inc @@ -0,0 +1,3 @@ +sub_filter '' ''; +sub_filter_once on; +sub_filter_types text/html; diff --git a/docker/nginx/conf.d/haanmind.net.conf b/docker/nginx/conf.d/haanmind.net.conf index 7ebff9f..6eef856 100644 --- a/docker/nginx/conf.d/haanmind.net.conf +++ b/docker/nginx/conf.d/haanmind.net.conf @@ -37,6 +37,7 @@ server { ssl_certificate_key /etc/letsencrypt/live/haanmind.net/privkey.pem; include /etc/nginx/conf.d/security-headers.inc; + include /etc/nginx/conf.d/goatcounter.inc; location /media/ { proxy_pass http://garage:3902/; diff --git a/docker/nginx/conf.d/hantim.net.conf b/docker/nginx/conf.d/hantim.net.conf index 094a1b8..3c1072f 100644 --- a/docker/nginx/conf.d/hantim.net.conf +++ b/docker/nginx/conf.d/hantim.net.conf @@ -37,6 +37,7 @@ server { ssl_certificate_key /etc/letsencrypt/live/hantim.net/privkey.pem; include /etc/nginx/conf.d/security-headers.inc; + include /etc/nginx/conf.d/goatcounter.inc; location /media/ { proxy_pass http://garage:3902/; diff --git a/docker/nginx/conf.d/hcsuzuki.music.conf b/docker/nginx/conf.d/hcsuzuki.music.conf index 9664df0..eb48a4e 100644 --- a/docker/nginx/conf.d/hcsuzuki.music.conf +++ b/docker/nginx/conf.d/hcsuzuki.music.conf @@ -37,6 +37,7 @@ server { ssl_certificate_key /etc/letsencrypt/live/hcsuzuki.music/privkey.pem; include /etc/nginx/conf.d/security-headers.inc; + include /etc/nginx/conf.d/goatcounter.inc; location /media/ { proxy_pass http://garage:3902/; diff --git a/docker/nginx/conf.d/hcsuzuki.net.conf b/docker/nginx/conf.d/hcsuzuki.net.conf index cef0b88..44f1ab4 100644 --- a/docker/nginx/conf.d/hcsuzuki.net.conf +++ b/docker/nginx/conf.d/hcsuzuki.net.conf @@ -37,6 +37,7 @@ server { ssl_certificate_key /etc/letsencrypt/live/hcsuzuki.net/privkey.pem; include /etc/nginx/conf.d/security-headers.inc; + include /etc/nginx/conf.d/goatcounter.inc; location /media/ { proxy_pass http://garage:3902/; diff --git a/docker/nginx/conf.d/thekims.family.conf b/docker/nginx/conf.d/thekims.family.conf index baf12d1..db92f90 100644 --- a/docker/nginx/conf.d/thekims.family.conf +++ b/docker/nginx/conf.d/thekims.family.conf @@ -37,6 +37,7 @@ server { ssl_certificate_key /etc/letsencrypt/live/thekims.family/privkey.pem; include /etc/nginx/conf.d/security-headers.inc; + include /etc/nginx/conf.d/goatcounter.inc; location /media/ { proxy_pass http://garage:3902/; diff --git a/docker/nginx/conf.d/timothykim.net.conf b/docker/nginx/conf.d/timothykim.net.conf index d09d1de..4d67e1e 100644 --- a/docker/nginx/conf.d/timothykim.net.conf +++ b/docker/nginx/conf.d/timothykim.net.conf @@ -37,6 +37,7 @@ server { ssl_certificate_key /etc/letsencrypt/live/timothykim.net/privkey.pem; include /etc/nginx/conf.d/security-headers.inc; + include /etc/nginx/conf.d/goatcounter.inc; location /media/ { proxy_pass http://garage:3902/; diff --git a/tools/app.sh b/tools/app.sh index 5e6d045..082b4a0 100755 --- a/tools/app.sh +++ b/tools/app.sh @@ -356,6 +356,7 @@ server { ssl_certificate_key /etc/letsencrypt/live/$APP/privkey.pem; include /etc/nginx/conf.d/security-headers.inc; + include /etc/nginx/conf.d/goatcounter.inc; location /media/ { proxy_pass http://garage:3902/;