Files
kontra/docs/KONTRA-GUIDE.md

7.2 KiB

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

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

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:
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)

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/)

# 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).