170 lines
7.6 KiB
Markdown
170 lines
7.6 KiB
Markdown
# 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)
|
||
> (pan/zoom, search, route tracing, light/dark themes)
|
||
|
||

|
||
|
||
```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 (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:
|
||
|
||
```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.* |