sam-4screen-desktop 2026-9-10:16:24:8
This commit is contained in:
@@ -1,6 +1,6 @@
|
|||||||
---
|
---
|
||||||
created: 2026-09-09 11:54
|
created: 2026-09-09 11:54
|
||||||
modified: 2026-09-09 12:05
|
modified: 2026-09-10 16:30
|
||||||
type: note
|
type: note
|
||||||
tags:
|
tags:
|
||||||
- website
|
- website
|
||||||
@@ -10,40 +10,26 @@ tags:
|
|||||||
- goth
|
- goth
|
||||||
- golang
|
- golang
|
||||||
- self-hosted
|
- self-hosted
|
||||||
|
- editing
|
||||||
|
- media
|
||||||
|
- admin
|
||||||
|
- ops
|
||||||
aliases:
|
aliases:
|
||||||
- Kontra
|
- Kontra
|
||||||
- Kontra Day
|
- Kontra Day
|
||||||
---
|
---
|
||||||
|
|
||||||
# Kontra
|
# Kontra — Editing, Media, Admin & Ops
|
||||||
|
|
||||||
> **Kontra** is a self-hosted news-opinion site engine. Content is plain Markdown in a git repo;
|
> **Kontra** is a self-hosted news-opinion site engine. Content + images are plain files in a git
|
||||||
> a single static Go binary renders it into a fast, SEO-friendly editorial site; a Decap CMS admin
|
> repo; a single static Go binary renders them into an SEO-friendly editorial site. Editors work in
|
||||||
> (authenticated with Gitea OAuth) lets editors work in the browser; and a git-driven pull loop
|
> **Obsidian** (daily) or **Decap** (browser). No database, no S3/object store.
|
||||||
> 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/
|
**Live:** https://kontra.lab.audasmedia.com.au
|
||||||
|
**Admin (Decap):** https://admin.kontra.lab.audasmedia.com.au/web/admin/
|
||||||
|
**Architecture map:** https://maps.lab.audasmedia.com.au/kontra_day/docs/kontra-architecture-map.html
|
||||||
**Repo (public):** https://gitea.lab.audasmedia.com.au/sam/kontra
|
**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)
|
|
||||||
|
|
||||||
```mermaid
|
```mermaid
|
||||||
flowchart LR
|
flowchart LR
|
||||||
subgraph editors["Content Editors"]
|
subgraph editors["Content Editors"]
|
||||||
@@ -51,132 +37,100 @@ flowchart LR
|
|||||||
N[Neovim / CLI] -->|git push| G
|
N[Neovim / CLI] -->|git push| G
|
||||||
D[Decap CMS<br/>admin.kontra.lab] -->|OAuth + git commit| G
|
D[Decap CMS<br/>admin.kontra.lab] -->|OAuth + git commit| G
|
||||||
end
|
end
|
||||||
|
subgraph gitea["Gitea (.35)"]
|
||||||
subgraph gitea["Gitea (192.168.20.35 :3001 Docker)"]
|
G[(sam/kontra-content<br/>md + images = origin)]
|
||||||
G[(sam/kontra-content<br/>markdown origin of truth)]
|
|
||||||
end
|
end
|
||||||
|
subgraph prod["Kontra container (.13 :8600)"]
|
||||||
subgraph prod["Kontra container (192.168.20.13 :8600 Docker)"]
|
P[git pull 30s] -->|restart on change| B
|
||||||
P[git pull loop<br/>every 30s] -->|restart on change| B
|
B[Go: templ+htmx+Goldmark]
|
||||||
B[Go binary<br/>templ + htmx + Goldmark]
|
|
||||||
B --> MD[(Markdown render<br/>front-matter + shortcodes)]
|
|
||||||
end
|
end
|
||||||
|
subgraph media["Media (git files)"]
|
||||||
subgraph media["Media (Garage S3 on .13)"]
|
F[(images beside articles)]
|
||||||
S3[(Garage S3<br/>kontra-day bucket)]
|
|
||||||
end
|
end
|
||||||
|
G -->|SSH| P
|
||||||
G -->|"SSH git"| P
|
B -->|"{{media:slug/file}}"| F
|
||||||
B -->|"shortcode: media"| S3
|
B -->|HTML| CB[Caddy .35 :443] --> WWW[browsers]
|
||||||
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](https://maps.lab.audasmedia.com.au/kontra_day/docs/kontra-architecture-map.html) ·
|
|
||||||
Rendered PNG: `docs/diagrams/kontra-architecture.png` · Mermaid source: `docs/kontra-architecture.mmd`
|
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
## How to use it
|
## 1. Content structure (rules)
|
||||||
|
|
||||||
### 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/
|
content/
|
||||||
├── config/site.yaml # site name, tagline, nav order
|
├── config/site.yaml
|
||||||
├── subjects/<slug>/
|
├── subjects/
|
||||||
│ ├── _subject.yaml # name, template, accent color
|
│ └── world/
|
||||||
│ └── <slug>.md # article (front matter + markdown)
|
│ ├── index.md # SUBJECT index (Decap folder marker, not an article)
|
||||||
└── pages/<slug>.md # standalone pages
|
│ ├── _subject.yaml
|
||||||
|
│ ├── chinese-chips/
|
||||||
|
│ │ ├── index.md # THE ARTICLE — file is always index.md
|
||||||
|
│ │ ├── hero.jpg # images beside their article
|
||||||
|
│ └── quantum-ai/
|
||||||
|
├── pages/about.md
|
||||||
|
└── _templates/article.md
|
||||||
```
|
```
|
||||||
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
|
- **Article file = `index.md` inside its own folder** (folder name = URL slug, lowercase).
|
||||||
- **Restart/redeploy:** `ssh sam@192.168.20.13` → `cd /home/sam/Docker/Containers/kontra && docker compose up -d --build`
|
- Lowercase filenames, no spaces. Images in the **same folder** as the article.
|
||||||
- **Pull loop** lives in `entrypoint.sh` inside the container (git pull every 30s, restart on change).
|
- `_`-prefixed files are skipped by the site (`_subject.yaml`, `_templates/`, `.trash/`).
|
||||||
- **Live site logs:** `docker logs -f kontra`
|
|
||||||
- **Content repo:** `/var/www/kontra_day/content` on .13 (mounted read-write into the container).
|
## 2. Editing
|
||||||
|
|
||||||
|
**Obsidian (daily):** open `/home/sam/obsidian/kontra_content` as a vault; **GitHub Sync** auto-pushes.
|
||||||
|
New article: **⌘⇧T → article** (substitutes `{{title}}`/`{{date}}` — hand-copying leaves them literal & breaks front-matter).
|
||||||
|
- `published: false` hides everywhere; omit = published. `featured: true` = hero.
|
||||||
|
|
||||||
|
**Decap (browser):** login with Gitea → nested tree `Articles → world → chinese-chips…`. Same result, commits to Gitea automatically.
|
||||||
|
|
||||||
|
## 3. Media
|
||||||
|
|
||||||
|
Images are **git files** beside their article (no S3).
|
||||||
|
- Body: ``
|
||||||
|
- Hero front-matter: `image: "{{media:chinese-chips/hero.jpg}}"` (**must be quoted**)
|
||||||
|
- Rule: `{{media:<slug>/<exact-file>}}` — lowercase slug, **exact filename incl. extension**, no leading slash, no backticks.
|
||||||
|
- App serves `/media/<path>` mapping slug→subject folder (safe-path guarded).
|
||||||
|
|
||||||
|
## 4. Admin (Decap) config
|
||||||
|
|
||||||
|
`app/src/web/admin/config.yml` (embedded at build). **Nested collections** list the 2-level tree:
|
||||||
|
```yaml
|
||||||
|
collections:
|
||||||
|
- name: articles
|
||||||
|
folder: subjects
|
||||||
|
create: true
|
||||||
|
slug: "{{slug}}"
|
||||||
|
nested: { depth: 100, subfolders: false } # subfolders:false REQUIRED
|
||||||
|
meta: { path: { widget: string, label: 'Path', index_file: 'index' } }
|
||||||
|
```
|
||||||
|
**Gotchas:** `index_file:'index'` not `_index`; `subfolders:false` not true; no apostrophes inside single-quoted labels (use double quotes); media library disabled (`media_folder:""`). Auth = Gitea OAuth (app `decap-kontra`, PKCE).
|
||||||
|
|
||||||
|
## 5. Ops
|
||||||
|
|
||||||
|
**Machines:** Gitea .35:3001 (origin) · Kontra container .13:8600 · Caddy master .35:443 · dev .27.
|
||||||
|
|
||||||
|
**Build** (after source changes, on .27): `cd app && ./build.sh`
|
||||||
|
|
||||||
|
**Deploy to .13:**
|
||||||
|
```bash
|
||||||
|
scp app/go.mod app/go.sum sam@192.168.20.13:/home/sam/Docker/Containers/kontra/src/
|
||||||
|
rsync -az --delete --exclude bin --exclude kontra-bin --exclude .git \
|
||||||
|
app/src/ sam@192.168.20.13:/home/sam/Docker/Containers/kontra/src/src/
|
||||||
|
ssh sam@192.168.20.13 'cd /home/sam/Docker/Containers/kontra && docker compose up -d --build'
|
||||||
|
```
|
||||||
|
- `.go.sum` must move with `go.mod`; use `--delete` (stale files like old `media.go` break builds).
|
||||||
|
- Content autosync: push → container pulls 30s → restart. No CI/CD.
|
||||||
|
- Secrets: Gitea token in `.env` (gitignored, chmod 600).
|
||||||
|
|
||||||
|
## 6. Backups
|
||||||
|
Content + images in git → backed by Gitea + production clone on .13. App source in git. Offsite per Backup Architecture.
|
||||||
|
|
||||||
|
## 7. For AI agents resuming
|
||||||
|
1. Read this file first.
|
||||||
|
2. Site is live; smallest change + `rsync --delete` deploy + verify on the live URL.
|
||||||
|
3. Treat `config.yml` as sacred (nested `subfolders:false` + `index_file:index`).
|
||||||
|
4. Media = git files; `{{media:<slug>/<file>}}`, quoted in front-matter, exact filename.
|
||||||
|
5. Don't reintroduce S3/Garage (dropped 2026-09-10).
|
||||||
|
|
||||||
---
|
---
|
||||||
|
*Full repo copy of this guide: `docs/KONTRA-GUIDE.md`. Architecture map published via Archify → maps.lab.*
|
||||||
## 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*
|
|
||||||
Reference in New Issue
Block a user