Files
obsidian-vault/200 projects/240 Web Sites/Kontra Day.md

8.3 KiB

created, modified, type, tags, aliases
created modified type tags aliases
2026-09-09 11:54 2026-09-09 12:05 note
website
politics
portfolio
kontra
goth
golang
self-hosted
Kontra
Kontra Day

Kontra

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 (title, author, date, kicker, subject, template, featured, …). 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 (your Gitea account, needs push access to sam/kontra-content).
  2. Articles collection: create/edit, set subject/kicker/excerpt/featured, write Markdown body, Save.
  3. Wait ~30 s — the site updates itself. No deploy step.

Or edit the files directly:

  • Obsidian vault: /home/sam/obsidian/kontra_content (symlink → content repo). Open as a vault, edit markdown, the Obsidian Git plugin pushes.
  • Neovim/CLI: clone ssh://git@gitea.lab.audasmedia.com.au:2222/sam/kontra-content.git, edit, git push.

Content model

content/
├── config/site.yaml          # site name, tagline, nav order
├── subjects/<slug>/
│   ├── _subject.yaml         # name, template, accent color
│   └── <slug>.md             # article (front matter + markdown)
└── pages/<slug>.md           # standalone pages

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

As an operator

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

What tools were used

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

Configs for the future (handover notes)

Networking / proxy

  • Master Caddy on .35 (/Docker/Containers/caddy/Caddyfile) → kontra.lab.audasmedia.com.au and admin.kontra.lab.audasmedia.com.au both reverse_proxy 192.168.20.13:8600.
  • The .13 Caddy (port 8000) is NOT involved for Kontra — it was reverted back to pre-Kontra config (backup: /etc/nixos/configuration.nix.bak-kontra-20260908-195305). Do not re-add Kontra to configuration.nix — it now runs in Docker (NixOS service files in /etc/systemd/system are read-only Nix-store managed).

Gitea / OAuth (Decap login)

  • OAuth app decap-kontra (client id ffe1375f-1a1b-40d2-9602-27b0f5b470fe, non-confidential/PKCE) — in Gitea → Settings → Applications.
  • Gitea CORS enabled for https://admin.kontra.lab.audasmedia.com.au (via GITEA__cors__* env in /Docker/Containers/gitea/docker-compose.yml).
  • Decap config: app/src/web/admin/config.yml (backend: name: gitea). Simple workflow only (Gitea backend does not support editorial_workflow PRs).

Environment / secrets

  • Gitea deploy token + API base: .env at repo root on .27 (gitignored — never commit). File perms 600.

Deploy specifics

  • Docker image builds Go + templ inside (golang:1.26-bookworm), runs on a slim runner. Port 8600.
  • Container mounts content (rw) + /home/sam/.ssh/id_ed25519 (ro) for the git pull to Gitea.
  • The 30s pull loop uses exec so the web-server PID is always the direct child (previous bug: nested subshell caused "address already in use" on restart — fixed).

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

  • Push new article → live in ~30 s. Push delete → gone in ~30 s. Clean restarts.
  • https://kontra.lab.audasmedia.com.au/ (front page with Topics grid), /articles/<slug>, /subjects/<slug> all 200.
  • Admin renders the Decap login page; "Login with Gitea" → OAuth flow works.

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