# Kontra β a self-hosted news-opinion site with automatic content sync
**Kontra** is a modern news/opinion publication engine: a single static Go binary that renders
markdown into a fast, SEO-friendly editorial site, a Decap CMS admin authenticated against
self-hosted Gitea, and a git-driven deploy pipeline that publishes content within ~30 seconds
of a save β all running on a home-lab network.
> **πΊ Interactive architecture map:** [maps.lab.audasmedia.com.au/kontra_day/docs/kontra-architecture-map.html](https://maps.lab.audasmedia.com.au/kontra_day/docs/kontra-architecture-map.html)
>
> **π Editing, media, admin & ops guide:** [`docs/KONTRA-GUIDE.md`](docs/KONTRA-GUIDE.md)
> (pan/zoom, search, route tracing, light/dark themes)

```mermaid
flowchart LR
subgraph editors["Content Editors"]
O[Obsidian vault
/obsidian/kontra_content] -->|git push| G
N[Neovim / CLI] -->|git push| G
D[Decap CMS
admin.kontra.lab] -->|OAuth + git commit| G
end
subgraph gitea["Gitea (192.168.20.35 :3001 Docker)"]
G[(sam/kontra-content
markdown origin of truth)]
end
subgraph prod["Kontra container (192.168.20.13 :8600 Docker)"]
P[git pull loop
every 30s] -->|restart on change| B
B[Go binary
templ + htmx + Goldmark]
B --> MD[(Markdown render
front-matter + shortcodes)]
end
subgraph media["Media (git files)"]
F[(Images beside articles
in the content repo)]
end
G -->|"SSH git"| P
B -->|"record: media"| F
B -->|"emits HTML"| CB[Caddy master
.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
```
---
## Why this project stands out
| | |
|---|---|
| **Zero-dependency production runtime** | One statically linked Go binary β no runtime, no Node, no PHP. 11 MB, ~10 MB RAM under 50 concurrent connections. |
| **Editorial workflow people actually use** | Editors edit markdown in Obsidian, Neovim, or a browser CMS β three independent edit paths that all converge on one git repo. |
| **Git is the content API** | Content has full history, rollback, branch review, and network-agnostic sync. No lock-in. |
| **Automatic publish pipeline** | `git push` β 30 s later the site is live. We prove it end-to-end (add, change, delete). |
| **Single-binary + embedded assets** | CSS, fonts, and the admin UI are compiled into the binary (`//go:embed`). Deploy = copy one file. |
| **Self-hosted, privacy-first** | Gitea + Caddy on own infra; media + content both live in git β no third-party SaaS, no object store to babysit. |
## The stack (GOTH)
| Layer | Technology | Why |
|---|---|---|
| **Language** | Go (golang) 1.26 | Compiled, fast, single static binary |
| **Templates** | templ v0.3 | Type-safe HTML components compiled to Go |
| **Interactivity** | htmx (server-rendered, no JS framework) | Progressive enhancement, SEO-safe |
| **Styling** | Tailwind-style **design tokens** (CSS custom properties) | Themeable via `DESIGN.md`, per-subject accent colours as data, not code |
| **Markdown** | goldmark (CommonMark 0.31) | Content + front-matter (`title`, `author`, `date`, `subject`, `kicker`, `featured`β¦) |
| **Admin** | Decap CMS + Obsidian | Both edit the same git repo; Obsidian for daily writing + media drop-in |
| **Media** | git files | Images live beside their article in the repo; `{{media:/}}` shortcodes resolved at render |
| **Git** | Gitea | Origin of truth; `sam/kontra-content` |
| **Proxy** | Caddy | TLS termination, hostname routing |
| **Deploy** | Docker + pull-loop | `git pull` every 30s; restart server on change |
## Architecture at a glance
```
Editors (Obsidian / Neovim / Decap) ββgit pushβββΆ Gitea (origin)
β SSH
βΌ
Kontra container (.13) βββgit pull every 30sβββ
β ββ Go binary (:8600)
β ββ Goldmark β HTML, {{media:}} β repo media file
β ββ restart on content change
βΌ
Caddy master (.35 :443) βββΆ readers (HTTPS)
```
## Repository layout
```
kontra/
βββ app/ # Go source (server.go, content.go, main.go, templates.templ)
β βββ src/
β β βββ *.templ # templ components (masthead, article, topics, mobile navβ¦)
β β βββ web/ # embedded assets: kontra.css (design tokens), admin/ (Decap)
β βββ build.sh # templ generate β go build
βββ content/ # the content repo (own Gitea remote: sam/kontra-content)
β βββ config/site.yaml
β βββ subjects//β¦ # markdown + front-matter per article
βββ docs/ # Mermaid + Archify maps, diagrams, published to maps.lab
βββ deploy/ # Docker, compose, entrypoint (pull-loop), revert scripts
βββ PLAN.md # Phase 1 decision record (stack, topology, storage)
βββ DESIGN.md # design tokens β Broadsheet Editorial system
```
## Content model
Markdown + YAML front-matter per page:
```yaml
---
title: The Quiet Reshaping of the Southern Harbor
author: M. Kestrel
date: 2026-09-08
kicker: World Dispatch
subject: world
template: article
featured: true
image: https://.../harbor1/1400/800
excerpt: A short deck under the headline.
---
Body markdown here⦠links, images via {{media:file.jpg}}, video via {{embed:vimeo:ID}}.
```
Template resolution: **page front-matter β subject `_subject.yaml` β site config β theme fallback**.
Front page = composable "Topics" grid (World / Politics / Culture / Techβ¦) whose order and
featured stories are data, not code.
## Design system
`DESIGN.md` defines a **Broadsheet Editorial** system: warm newsprint palette, Newsreader
(headlines) / Literata (body) / Work Sans (UI) type scale, hairline rules, sharp corners,
no shadow-depth.
- Implemented as CSS custom properties (`--canvas`, `--ink`, `--accent`, β¦) β themeable data.
- Per-subject accent overrides via subject config.
- Responsive: β₯1024 / 641β1023 / β€640, with dropdown topic nav + mobile bottom-nav.
- Fully working clickable prototype under `prototype/`.
## Deploy & ops
- Live: `https://kontra.lab.audasmedia.com.au` Β· Admin: `https://admin.kontra.lab.audasmedia.com.au/web/admin/`
- Container: `kontra` on `.13:8600`, mounts content + SSH key, `restart: unless-stopped`
- Content autosync: entrypoint runs `git pull` every 30s, restarts the binary on HEAD change.
- Master route: Caddy on `.35` terminates TLS and `reverse_proxy 192.168.20.13:8600`.
- Build: `app/build.sh` (templ + go) or the multi-stage Dockerfile (builds golang + templ in-image).
## Security notes
- Decap β Gitea OAuth with **non-confidential (PKCE)** application, redirect URI pinned to the admin origin.
- Gitea CORS scoped to the admin origin; content repo private.
- Deploy token kept in a gitignored `.env`; no secrets in any repo.
- Content repo requires push access to publish β editorial permissions map to Gitea team access.
## Docs & maps
| Artifact | Path / URL |
|---|---|
| Mermaid source | `docs/kontra-architecture.mmd` |
| Architecture doc | `docs/architecture.md` |
| Interactive map | `docs/kontra-architecture-map.html` Β· [maps.lab.audasmedia.com.au/kontra_day/docs/](https://maps.lab.audasmedia.com.au/kontra_day/docs/) |
| Plan (decisions) | `PLAN.md` |
| Design system | `DESIGN.md` |
---
*Built with the GOTH stack β Go, templ, htmx, Tailwind-tokens β content + media in git, self-hosted on Gitea + Caddy + Docker.*