8.3 KiB
created, modified, type, tags, aliases
| created | modified | type | tags | aliases | |||||||||
|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
| 2026-09-09 11:54 | 2026-09-09 12:05 | note |
|
|
Kontra
Kontra is a self-hosted news-opinion site engine. Content is plain Markdown in a git repo; a single static Go binary renders it into a fast, SEO-friendly editorial site; a Decap CMS admin (authenticated with Gitea OAuth) lets editors work in the browser; and a git-driven pull loop publishes changes live within ~30 seconds of a save.
Live: https://kontra.lab.audasmedia.com.au · Admin: https://admin.kontra.lab.audasmedia.com.au/web/admin/ Repo (public): https://gitea.lab.audasmedia.com.au/sam/kontra
🗺 Interactive architecture map: https://maps.lab.audasmedia.com.au/kontra_day/docs/kontra-architecture-map.html (pan/zoom, search, route tracing, dark/light themes)
What it is
A complete, self-hosted news/opinion publishing platform designed to run on the home lab:
- Markdown-first content — articles are
.mdfiles with YAML front-matter (title,author,date,kicker,subject,template,featured, …). No database to manage. - One static binary — Go (golang) + templ + htmx + design-token CSS. No runtime, no Node, no PHP.
- Three edit paths — Obsidian vault, Neovim/CLI, or the Decap CMS browser editor. All converge on one git repo.
- Git is the API — full history, rollback, branch review, backups included.
- Autopublish —
git push→ container pulls every 30s → live. Proven for add + delete. - Self-hosted everything — Gitea (origin), Garage S3 (media), Caddy (TLS), Decap (admin), Docker (runtime).
Architecture (Mermaid)
flowchart LR
subgraph editors["Content Editors"]
O[Obsidian vault<br/>/obsidian/kontra_content] -->|git push| G
N[Neovim / CLI] -->|git push| G
D[Decap CMS<br/>admin.kontra.lab] -->|OAuth + git commit| G
end
subgraph gitea["Gitea (192.168.20.35 :3001 Docker)"]
G[(sam/kontra-content<br/>markdown origin of truth)]
end
subgraph prod["Kontra container (192.168.20.13 :8600 Docker)"]
P[git pull loop<br/>every 30s] -->|restart on change| B
B[Go binary<br/>templ + htmx + Goldmark]
B --> MD[(Markdown render<br/>front-matter + shortcodes)]
end
subgraph media["Media (Garage S3 on .13)"]
S3[(Garage S3<br/>kontra-day bucket)]
end
G -->|"SSH git"| P
B -->|"shortcode: media"| S3
B -->|"emits HTML"| CB[Caddy master<br/>.35 :80/:443]
CB -->|"reverse_proxy :8600"| B
subgraph net["Network"]
CB -->|https| WWW[kontra.lab.audasmedia.com.au]
CB -->|https| ADM[admin.kontra.lab.audasmedia.com.au]
end
WWW -->|browser| B
ADM -->|browser| D
Interactive version: maps.lab ·
Rendered PNG: docs/diagrams/kontra-architecture.png · Mermaid source: docs/kontra-architecture.mmd
How to use it
As an editor
- Open https://admin.kontra.lab.audasmedia.com.au/web/admin/ → Login with Gitea (your Gitea account, needs push access to
sam/kontra-content). - Articles collection: create/edit, set subject/kicker/excerpt/featured, write Markdown body, Save.
- Wait ~30 s — the site updates itself. No deploy step.
Or edit the files directly:
- Obsidian vault:
/home/sam/obsidian/kontra_content(symlink → content repo). Open as a vault, edit markdown, the Obsidian Git plugin pushes. - Neovim/CLI: clone
ssh://git@gitea.lab.audasmedia.com.au:2222/sam/kontra-content.git, edit,git push.
Content model
content/
├── config/site.yaml # site name, tagline, nav order
├── subjects/<slug>/
│ ├── _subject.yaml # name, template, accent color
│ └── <slug>.md # article (front matter + markdown)
└── pages/<slug>.md # standalone pages
Template resolution: page front-matter → subject _subject.yaml → site config → theme fallback.
Media: {{media:file.jpg}} (→ Garage S3 bucket), {{embed:vimeo:ID}} (→ video embed).
As an operator
- Restart/redeploy:
ssh sam@192.168.20.13→cd /home/sam/Docker/Containers/kontra && docker compose up -d --build - Pull loop lives in
entrypoint.shinside the container (git pull every 30s, restart on change). - Live site logs:
docker logs -f kontra - Content repo:
/var/www/kontra_day/contenton .13 (mounted read-write into the container).
What tools were used
| Tool | Role | Where |
|---|---|---|
| Go (golang) 1.26 | App language — one static binary | app/ |
| templ v0.3 | HTML template compile-to-Go | app/src/*.templ |
| htmx | Server-rendered interactivity | templates |
| goldmark | CommonMark Markdown renderer | app/src/content.go |
| goccy/go-yaml | Front-matter / config parsing | app/src/content.go |
| Decap CMS | Browser admin | app/src/web/admin/ (embedded), served at /web/admin/ |
| Gitea | Git origin (both repos: sam/kontra, sam/kontra-content) |
.35 Docker :3001 |
| Garage S3 | Media objects (kontra-day bucket) |
.13 Docker :3900 |
| Caddy | TLS + routing, master reverse proxy | .35 Docker :80/:443 |
| Docker / compose | Runtime container kontra-kontra |
.13 |
| Design tokens (CSS vars) | Theming from DESIGN.md |
app/src/web/assets/kontra.css |
| Mermaid + Archify | Architecture docs + interactive map | docs/ |
| Obsidian Git | Vault auto-push | .27 |
Where things are
| Thing | Location |
|---|---|
| App source + deploy + docs | /home/sam/home_network/web_sites/kontra_day/ (.27) |
| Public repo (README, app, docs) | https://gitea.lab.audasmedia.com.au/sam/kontra |
| Content repo (origin) | https://gitea.lab.audasmedia.com.au/sam/kontra-content |
| Production content clone | .13:/var/www/kontra_day/content |
| Docker project | .13:/home/sam/Docker/Containers/kontra/ |
| Binary (host build) | app/bin/ after app/build.sh on .27 |
| Obsidian vault | /home/sam/obsidian/kontra_content |
| Interactive map | https://maps.lab.audasmedia.com.au/kontra_day/docs/ |
| Plan/decisions | PLAN.md (repo) |
| Design system | DESIGN.md (repo) |
Configs for the future (handover notes)
Networking / proxy
- Master Caddy on .35 (
/Docker/Containers/caddy/Caddyfile) →kontra.lab.audasmedia.com.auandadmin.kontra.lab.audasmedia.com.aubothreverse_proxy 192.168.20.13:8600. - The .13 Caddy (port 8000) is NOT involved for Kontra — it was reverted back to pre-Kontra config (backup:
/etc/nixos/configuration.nix.bak-kontra-20260908-195305). Do not re-add Kontra to configuration.nix — it now runs in Docker (NixOS service files in/etc/systemd/systemare read-only Nix-store managed).
Gitea / OAuth (Decap login)
- OAuth app
decap-kontra(client id ffe1375f-1a1b-40d2-9602-27b0f5b470fe, non-confidential/PKCE) — in Gitea → Settings → Applications. - Gitea CORS enabled for
https://admin.kontra.lab.audasmedia.com.au(viaGITEA__cors__*env in/Docker/Containers/gitea/docker-compose.yml). - Decap config:
app/src/web/admin/config.yml(backend: name: gitea). Simple workflow only (Gitea backend does not support editorial_workflow PRs).
Environment / secrets
- Gitea deploy token + API base:
.envat repo root on .27 (gitignored — never commit). File perms 600.
Deploy specifics
- Docker image builds Go + templ inside (
golang:1.26-bookworm), runs on a slim runner. Port 8600. - Container mounts content (rw) +
/home/sam/.ssh/id_ed25519(ro) for the git pull to Gitea. - The 30s pull loop uses
execso the web-server PID is always the direct child (previous bug: nested subshell caused "address already in use" on restart — fixed).
Known-good state (verified 2026-09-09)
- Push new article → live in ~30 s. Push delete → gone in ~30 s. Clean restarts.
https://kontra.lab.audasmedia.com.au/(front page with Topics grid),/articles/<slug>,/subjects/<slug>all 200.- Admin renders the Decap login page; "Login with Gitea" → OAuth flow works.
Portfolio/showcase link: https://gitea.lab.audasmedia.com.au/sam/kontra (public) — README includes the interactive map, stack, and deploy story. Map: https://maps.lab.audasmedia.com.au/kontra_day/docs/kontra-architecture-map.html