docs(guide): add KONTRA-GUIDE.md (editing/media/admin/ops); update archify+mermaid to git-files media (drop Garage)

This commit is contained in:
sam
2026-09-10 16:20:49 +10:00
parent e578f1278f
commit 9b068bc48e
6 changed files with 169 additions and 12 deletions

157
docs/KONTRA-GUIDE.md Normal file
View File

@@ -0,0 +1,157 @@
# 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).

View File

@@ -123,8 +123,8 @@
{ {
"id": "garage", "id": "garage",
"type": "cloud", "type": "cloud",
"label": "Garage S3", "label": "Git media",
"sublabel": "kontra-day bucket", "sublabel": "images beside articles",
"pos": [ "pos": [
680, 680,
160 160
@@ -310,7 +310,7 @@
"title": "Auth & media", "title": "Auth & media",
"items": [ "items": [
"Decap CMS logs in with Gitea OAuth (PKCE)", "Decap CMS logs in with Gitea OAuth (PKCE)",
"Media resolves from Garage S3 via {{media:}} shortcodes" "Media = git files beside articles; {{media:slug/file}}"
] ]
} }
] ]

Binary file not shown.

Before

Width:  |  Height:  |  Size: 75 KiB

After

Width:  |  Height:  |  Size: 75 KiB

File diff suppressed because one or more lines are too long

Before

Width:  |  Height:  |  Size: 34 KiB

After

Width:  |  Height:  |  Size: 34 KiB

View File

@@ -5007,15 +5007,15 @@
<text data-detail="fine" x="760" y="360" class="t-backend" font-size="7" text-anchor="middle">:8600 Docker</text> <text data-detail="fine" x="760" y="360" class="t-backend" font-size="7" text-anchor="middle">:8600 Docker</text>
</g> </g>
<g id="node-garage" data-node-id="garage" data-node-label="Garage S3" tabindex="0" role="button" aria-label="Focus Garage S3, kontra-day bucket, Kontra container (.13 :8600)" aria-pressed="false" data-node-kind="cloud" data-node-sublabel="kontra-day bucket" data-node-context="Kontra container (.13 :8600)"> <g id="node-garage" data-node-id="garage" data-node-label="Git media" tabindex="0" role="button" aria-label="Focus Git media, images beside articles, Kontra container (.13 :8600)" aria-pressed="false" data-node-kind="cloud" data-node-sublabel="images beside articles" data-node-context="Kontra container (.13 :8600)">
<title>Garage S3 · kontra-day bucket · Kontra container (.13 :8600)</title> <title>Git media · images beside articles · Kontra container (.13 :8600)</title>
<rect x="680" y="160" width="150" height="64" rx="6" class="c-mask"/> <rect x="680" y="160" width="150" height="64" rx="6" class="c-mask"/>
<rect x="680" y="160" width="150" height="64" rx="6" class="c-cloud" stroke-width="1.5"/> <rect x="680" y="160" width="150" height="64" rx="6" class="c-cloud" stroke-width="1.5"/>
<g aria-hidden="true" data-semantic-sigil="cloud" class="semantic-sigil s-cloud" transform="translate(686 166) scale(0.6875)"> <g aria-hidden="true" data-semantic-sigil="cloud" class="semantic-sigil s-cloud" transform="translate(686 166) scale(0.6875)">
<path d="M4.3 12.5h7.3a2.4 2.4 0 0 0 .2-4.8 4 4 0 0 0-7.5-1.3A3.1 3.1 0 0 0 4.3 12.5Z"/> <path d="M4.3 12.5h7.3a2.4 2.4 0 0 0 .2-4.8 4 4 0 0 0-7.5-1.3A3.1 3.1 0 0 0 4.3 12.5Z"/>
</g> </g>
<text data-detail-anchor x="755" y="190" class="t-primary" font-size="11" font-weight="600" text-anchor="middle">Garage S3</text> <text data-detail-anchor x="755" y="190" class="t-primary" font-size="11" font-weight="600" text-anchor="middle">Git media</text>
<text data-detail="context" x="755" y="206" class="t-muted" font-size="9" text-anchor="middle">kontra-day bucket</text> <text data-detail="context" x="755" y="206" class="t-muted" font-size="9" text-anchor="middle">images beside articles</text>
</g> </g>
<g id="node-caddy" data-node-id="caddy" data-node-label="Caddy master" tabindex="0" role="button" aria-label="Focus Caddy master, :80/:443 reverse proxy, Edge (Caddy .35)" aria-pressed="false" data-node-kind="cloud" data-node-sublabel=":80/:443 reverse proxy" data-node-context="Edge (Caddy .35)"> <g id="node-caddy" data-node-id="caddy" data-node-label="Caddy master" tabindex="0" role="button" aria-label="Focus Caddy master, :80/:443 reverse proxy, Edge (Caddy .35)" aria-pressed="false" data-node-kind="cloud" data-node-sublabel=":80/:443 reverse proxy" data-node-context="Edge (Caddy .35)">
@@ -5317,7 +5317,7 @@
</div> </div>
<ul> <ul>
<li>&bull; Decap CMS logs in with Gitea OAuth (PKCE)</li> <li>&bull; Decap CMS logs in with Gitea OAuth (PKCE)</li>
<li>&bull; Media resolves from Garage S3 via {{media:}} shortcodes</li> <li>&bull; Media = git files beside articles; {{media:slug/file}}</li>
</ul> </ul>
</div> </div>
</div> </div>

View File

@@ -15,12 +15,12 @@ flowchart LR
B --> MD[(Markdown render<br/>front-matter + shortcodes)] B --> MD[(Markdown render<br/>front-matter + shortcodes)]
end end
subgraph media["Media (Garage S3 on .13)"] subgraph media["Media (git files)"]
S3[(Garage S3<br/>kontra-day bucket)] F[(Images beside articles<br/>in the content repo)]
end end
G -->|"SSH git"| P G -->|"SSH git"| P
B -->|"shortcode: media"| S3 B -->|"{{media:slug/file}}"| F
B -->|"emits HTML"| CB[Caddy master<br/>.35 :80/:443] B -->|"emits HTML"| CB[Caddy master<br/>.35 :80/:443]
CB -->|"reverse_proxy :8600"| B CB -->|"reverse_proxy :8600"| B