# 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<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 (.35 Docker)"]
    G[(sam/kontra-content<br/>markdown + images = origin)]
  end
  subgraph prod["Kontra container (.13 :8600)"]
    P[git pull loop<br/>every 30s] -->|restart on change| B
    B[Go binary: templ + htmx + Goldmark]
  end
  subgraph media["Media (git files)"]
    F[(Images beside articles<br/>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/<subject>/<slug>/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>/<exact-filename>}}` — slug lowercase, **exact filename incl. extension** (`.jpg` ≠ `.jpeg`), **no leading slash**, **no backticks** around a real image markdown line.

The app serves `/media/<path>` 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:<slug>/<file>}}`, quoted in front-matter, exact filename.
5. Don't reintroduce S3/Garage for media (dropped 2026-09-10).