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

📖 Editing, media, admin & ops guide: docs/KONTRA-GUIDE.md (pan/zoom, search, route tracing, light/dark themes)

Kontra architecture

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 (git files)"]
    F[(Images beside articles<br/>in the content repo)]
  end

  G -->|"SSH git"| P
  B -->|"record: media"| F
  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

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:<slug>/<file>}} 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/<slug>/…  # 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:

---
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/
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.

Description
Kontra — GOTH news-opinion site (Go + templ + htmx). App source.
Readme 719 KiB
Languages
CSS 34.5%
HTML 24.5%
Go 16.9%
Shell 12.3%
templ 8.8%
Other 3%