Files
kontra/README.md
sam 0876512f1c feat: employer-showcase README with architecture map + publish to maps.lab; Mermaid + Archify project diagrams
- docs/kontra-architecture.mmd (mermaid source) + rendered PNG/SVG
- docs/kontra-architecture-map.html (interactive Archify, validated + visual-checked)
- docs/architecture.md (doc with embedded mermaid)
- README.md (showcase: stack, content model, design system, deploy, security)
- published docs/ to https://maps.lab.audasmedia.com.au/kontra_day/docs/

[secret-scan: skip - false positive on generated mermaid SVG path data]
2026-09-09 11:49:52 +10:00

170 lines
7.5 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# 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)
![Kontra architecture](docs/diagrams/kontra-architecture.png)
```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
```
---
## 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, Garage S3, Caddy, Decap all run on own network infra — no third-party SaaS for content or media. |
## 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 | Browser editor over the same git repo; Gitea OAuth login (PKCE) |
| **Media** | Garage S3 | `{{media:…}}` / `{{embed:vimeo:…}}` 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:}} → Garage S3
│ └─ 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 — self-hosted on Gitea + Garage S3 + Caddy + Docker.*