# Kontra — Editing, Media, Admin & Ops Single source of truth for working with the Kontra site. Covers: how content is structured, how to edit it (Obsidian or Decap), how media works, how the admin is configured, and how the whole thing runs + deploys. **Live site:** 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 ```mermaid flowchart LR subgraph editors["Content Editors"] O[Obsidian vault
/obsidian/kontra_content] -->|git push| G N[Neovim / CLI] -->|git push| G D[Decap CMS
admin.kontra.lab] -->|OAuth + git commit| G end subgraph gitea["Gitea (.35 Docker)"] G[(sam/kontra-content
markdown + images = origin)] end subgraph prod["Kontra container (.13 :8600)"] P[git pull loop
every 30s] -->|restart on change| B B[Go binary: templ + htmx + Goldmark] end subgraph media["Media (git files)"] F[(Images beside articles
in the content repo)] end G -->|"SSH git"| P B -->|"{{media:...}} shortcode"| F B -->|"HTML"| CB[Caddy master (.35 :443)] CB --> WWW[browsers] D -->|login| G ``` ## 1. Content structure (the rules) ``` content/ ├── config/site.yaml # site name, tagline, nav (lowercase keys) ├── subjects/ │ ├── world/ │ │ ├── index.md # SUBJECT index — Decap folder marker (not an article) │ │ ├── _subject.yaml # subject config (name, template, color, ALL lowercase keys) │ │ ├── chinese-chips/ │ │ │ ├── index.md # THE ARTICLE (file is always index.md inside its folder) │ │ │ ├── hero.jpg # images live beside their article │ │ │ └── figure-2.jpg │ │ └── quantum-ai/… │ └── tech/… ├── pages/ # standalone pages (about, 404…) │ └── about.md └── _templates/ └── article.md # Obsidian insert-template (⌘⇧T) ``` **Hard rules** - Article file = **`index.md` inside its own folder** named exactly like the slug (`subjects///index.md`). The folder name = the URL slug (lowercased). - Filenames in a folder: **lowercase, no spaces** (`quantum-ai/`, not `Quantum-ai/`). - Images go in the **same folder as the article** — never the repo root, never a shared folder. - `_`-prefixed files are skipped by the site (`_subject.yaml`, `_templates/`, `.trash/`). ## 2. Editing content ### A. Obsidian (recommended daily editor) Open **`/home/sam/obsidian/kontra_content`** (a symlink to the content repo) as a vault. The **GitHub Sync** plugin auto-pushes on save (~15s). - **New article:** insert the template via **⌘⇧T → "article"** — this substitutes `{{title}}`/`{{date}}`. (If you hand-copy the template file, those variables stay literal and break front-matter.) - **New subject:** create a folder under `subjects/` with an `index.md` (used as the subject index) and `_subject.yaml`. - **Publish/hide:** `published: true` → live; `published: false` → hidden everywhere (404 + removed from all lists). Omit = published. - **Featured:** `featured: true` puts it in the hero lead slot. ### B. Decap (browser admin) Login with **Gitea** → you get a nested tree: `Articles → world → chinese-chips …`. Click an article to edit. Same file → same result. **No manual push needed** — Decap commits to Gitea, autosync publishes. ## 3. Media (images) Images are **git files beside their article** — no S3/object store. ``` subjects/world/chinese-chips/ index.md hero.jpg ← dragged into Obsidian (or placed in the folder) ``` **Referencing** - Body: `![Alt]({{media:chinese-chips/hero.jpg}})` - Front-matter hero: `image: "{{media:chinese-chips/hero.jpg}}"` ← **must be quoted** - Rule: `{{media:/}}` — slug lowercase, **exact filename incl. extension** (`.jpg` ≠ `.jpeg`), **no leading slash**, **no backticks** around a real image markdown line. The app serves `/media/` by mapping the slug→subject folder (safe-path guarded). ## 4. The admin (Decap) config - Lives at **`app/src/web/admin/config.yml`** (embedded into the binary at build). - **Nested collections** — this is how it lists the 2-level tree. Do NOT "simplify" it: ```yaml collections: - name: articles label: Articles folder: subjects create: true slug: "{{slug}}" nested: { depth: 100, subfolders: false } # subfolders:false is REQUIRED meta: { path: { widget: string, label: 'Path', index_file: 'index' } } ``` - **Decap gotchas (do not repeat):** - `index_file: 'index'` (not `_index`), `subfolders: false` (not true) - Labels in single quotes: NO apostrophes inside (breaks YAML — use double quotes) - The media library is disabled (`media_folder: ""`) — images are git files, not uploaded through Decap. - `subjects` + `pages` collections also exist. Auth is **Gitea OAuth** (app `decap-kontra`, PKCE). ## 5. Ops — how it runs **Machines** | Machine | IP | Role | |---|---|---| | Gitea (origin) | .35 :3001 | `sam/kontra-content` (content+images), `sam/kontra` (app) | | Kontra container | .13 :8600 | Go binary; pulls content every 30s + restarts on change | | Caddy master | .35 :443 | TLS; `kontra.lab…` + `admin.kontra…` → `.13:8600` | | Dev (.27) | .27 | Obsidian vault + app source + build | **Build** (after source changes, on .27) ```bash cd app && ./build.sh # templ generate → go build → app/kontra-bin ``` The container builds inside Docker (golang stage) — update `config.yml`/assets then redeploy. **Deploy to .13** (the container runs from `/home/sam/Docker/Containers/kontra/`) ```bash # source (needed bits) 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`; and use `--delete` or stale files (like old `media.go`) break the build. **Content autosync** — pushes to `sam/kontra-content` are pulled by the container every 30s; server restarts on change. No CI/CD needed. **Secrets** — the Gitea deploy token lives in `.env` (repo root, **gitignored**, chmod 600). Never commit. ## 6. Backups - Content + images are in the git repo → backed up by Gitea (mirrors + clients) and the production clone on `.13`. - The app source lives in git on Gitea. - House-wide offsite backup covers these hosts per the Backup Architecture plan. ## 7. For AI agents resuming this project 1. Read this file first. 2. The site is **live** and the pipeline works — prefer the smallest change + `rsync --delete` deploy + verify on `https://kontra.lab.audasmedia.com.au`. 3. Decap config: 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 for media (dropped 2026-09-10).