# 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) ![Kontra architecture](docs/diagrams/kontra-architecture.png) ```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.*