From 1e2358708c9ed5672d560822d340011cc976e4b6 Mon Sep 17 00:00:00 2001 From: Sam Rolfe Date: Thu, 10 Sep 2026 16:24:08 +1000 Subject: [PATCH] sam-4screen-desktop 2026-9-10:16:24:8 --- 200 projects/240 Web Sites/Kontra Day.md | 236 +++++++++-------------- 1 file changed, 95 insertions(+), 141 deletions(-) diff --git a/200 projects/240 Web Sites/Kontra Day.md b/200 projects/240 Web Sites/Kontra Day.md index 95cb852..4db49b3 100644 --- a/200 projects/240 Web Sites/Kontra Day.md +++ b/200 projects/240 Web Sites/Kontra Day.md @@ -1,6 +1,6 @@ --- created: 2026-09-09 11:54 -modified: 2026-09-09 12:05 +modified: 2026-09-10 16:30 type: note tags: - website @@ -10,40 +10,26 @@ tags: - goth - golang - self-hosted + - editing + - media + - admin + - ops aliases: - Kontra - 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; -> 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. +> **Kontra** is a self-hosted news-opinion site engine. Content + images are plain files in a git +> repo; a single static Go binary renders them into an SEO-friendly editorial site. Editors work in +> **Obsidian** (daily) or **Decap** (browser). No database, no S3/object store. -**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 -> πŸ—Ί **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 flowchart LR subgraph editors["Content Editors"] @@ -51,132 +37,100 @@ flowchart LR N[Neovim / CLI] -->|git push| G D[Decap CMS
admin.kontra.lab] -->|OAuth + git commit| G end - - subgraph gitea["Gitea (192.168.20.35 :3001 Docker)"] - G[(sam/kontra-content
markdown origin of truth)] + subgraph gitea["Gitea (.35)"] + G[(sam/kontra-content
md + images = origin)] end - - subgraph prod["Kontra container (192.168.20.13 :8600 Docker)"] - P[git pull loop
every 30s] -->|restart on change| B - B[Go binary
templ + htmx + Goldmark] - B --> MD[(Markdown render
front-matter + shortcodes)] + subgraph prod["Kontra container (.13 :8600)"] + P[git pull 30s] -->|restart on change| B + B[Go: templ+htmx+Goldmark] end - - subgraph media["Media (Garage S3 on .13)"] - S3[(Garage S3
kontra-day bucket)] + subgraph media["Media (git files)"] + F[(images beside articles)] end - - G -->|"SSH git"| P - B -->|"shortcode: media"| S3 - B -->|"emits HTML"| CB[Caddy master
.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 + G -->|SSH| P + B -->|"{{media:slug/file}}"| F + B -->|HTML| CB[Caddy .35 :443] --> WWW[browsers] ``` -**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/ -β”œβ”€β”€ config/site.yaml # site name, tagline, nav order -β”œβ”€β”€ subjects// -β”‚ β”œβ”€β”€ _subject.yaml # name, template, accent color -β”‚ └── .md # article (front matter + markdown) -└── pages/.md # standalone pages +β”œβ”€β”€ config/site.yaml +β”œβ”€β”€ subjects/ +β”‚ └── world/ +β”‚ β”œβ”€β”€ index.md # SUBJECT index (Decap folder marker, not an article) +β”‚ β”œβ”€β”€ _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 -- **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). +- **Article file = `index.md` inside its own folder** (folder name = URL slug, lowercase). +- Lowercase filenames, no spaces. Images in the **same folder** as the article. +- `_`-prefixed files are skipped by the site (`_subject.yaml`, `_templates/`, `.trash/`). + +## 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: `![Alt]({{media:chinese-chips/hero.jpg}})` +- Hero front-matter: `image: "{{media:chinese-chips/hero.jpg}}"` (**must be quoted**) +- Rule: `{{media:/}}` β€” lowercase slug, **exact filename incl. extension**, no leading slash, no backticks. +- App serves `/media/` 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:/}}`, quoted in front-matter, exact filename. +5. Don't reintroduce S3/Garage (dropped 2026-09-10). --- - -## 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/`, `/subjects/` 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* \ No newline at end of file +*Full repo copy of this guide: `docs/KONTRA-GUIDE.md`. Architecture map published via Archify β†’ maps.lab.* \ No newline at end of file