Files
kontra/docs/OVERVIEW.md

6.8 KiB

Kontra — Project Overview

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 .md files with YAML front-matter. 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

  1. Open https://admin.kontra.lab.audasmedia.com.au/web/admin/ → Login with Gitea (needs push access to sam/kontra-content).
  2. Articles collection: create/edit, set subject/kicker/excerpt/featured, write Markdown body, Save. Wait ~30 s — the site updates.

Or edit files directly: Obsidian vault (/home/sam/obsidian/kontra_content) or clone ssh://git@gitea.lab.audasmedia.com.au:2222/sam/kontra-content.git, edit, push.

Content model

content/
├── config/site.yaml          # site name, tagline, nav order
├── subjects/<slug>/
│   ├── _subject.yaml         # name, template, accent color
│   └── <slug>.md             # article
└── pages/<slug>.md

Template resolution: page front-matter → subject _subject.yaml → site config → fallback. Media: {{media:file.jpg}} (Garage S3), {{embed:vimeo:ID}}.

As an operator

  • Restart/redeploy: ssh sam@192.168.20.13 → cd /home/sam/Docker/Containers/kontra && docker compose up -d --build
  • Pull loop: entrypoint.sh in the container (git pull every 30s, restart on change).
  • Logs: docker logs -f kontra
  • Content: /var/www/kontra_day/content on .13 (rw mount into container).

Tools used

Tool Role Where
Go (golang) 1.26 App language — one static binary app/
templ v0.3 HTML templates compile-to-Go app/src/*.templ
htmx Server-rendered interactivity templates
goldmark CommonMark renderer app/src/content.go
goccy/go-yaml Front-matter/config parsing app/src/content.go
Decap CMS Browser admin app/src/web/admin/
Gitea Git origin (both repos) .35 Docker :3001
Garage S3 Media (kontra-day bucket) .13 Docker :3900
Caddy TLS + routing .35 :80/:443
Docker / compose Runtime container .13
Design tokens (CSS vars) Theming from DESIGN.md app/src/web/assets/kontra.css
Mermaid + Archify 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 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/
Obsidian vault /home/sam/obsidian/kontra_content
Interactive map https://maps.lab.audasmedia.com.au/kontra_day/docs/
Plan/decisions PLAN.md
Design system DESIGN.md

Configs for the future (handover notes)

Networking

  • Master Caddy (.35, /Docker/Containers/caddy/Caddyfile) → both kontra.lab… and admin.kontra… → reverse_proxy 192.168.20.13:8600.
  • .13 Caddy config is back to pre-Kontra. Kontra runs in Docker — do not re-add to configuration.nix (NixOS system units are Nix-store-managed).

Gitea / OAuth

  • OAuth app decap-kontra (non-confidential / PKCE) in Gitea → Settings → Applications.
  • Gitea CORS enabled for the admin origin (GITEA__cors__* in /Docker/Containers/gitea/docker-compose.yml).
  • Decap config.yml: backend: name: gitea, simple workflow (no editorial workflow PRs — Gitea backend limitation).

Secrets

  • Gitea deploy token + API base: .env at repo root (.27), gitignored, never commit. chmod 600.

Deploy specifics

  • Docker image builds Go+templ in-image; runner is slim. Port 8600.
  • Container mounts content (rw) + /home/sam/.ssh/id_ed25519 (ro) for the pull.
  • Pull loop uses exec so the server PID is always direct (prior nested-subshell bug caused "address already in use" on restart — fixed).

Known-good state (verified 2026-09-09)

  • Add/delete content → live in ~30 s. Clean restarts.
  • Front page, /articles/<slug>, /subjects/<slug> all 200.
  • Admin renders Decap login; "Login with Gitea" OAuth flow works.

Portfolio/showcase: 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