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.mdinside 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/, notQuantum-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 anindex.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: trueputs 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:
 - 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+pagescollections also exist. Auth is Gitea OAuth (appdecap-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.summust move withgo.mod; and use--deleteor stale files (like oldmedia.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
- Read this file first.
- The site is live and the pipeline works — prefer the smallest change +
rsync --deletedeploy + verify onhttps://kontra.lab.audasmedia.com.au. - Decap config: treat
config.ymlas sacred (nestedsubfolders:false+index_file:index). - Media = git files.
{{media:<slug>/<file>}}, quoted in front-matter, exact filename. - Don't reintroduce S3/Garage for media (dropped 2026-09-10).