# Plan — Robust media: images co-located with articles, git-as-source-of-truth Status: **PROPOSED** — awaiting your review, then I implement exactly this. ## 0. Problem statement (why we're here) The S3/Garage + Decap-media path produced a fragile system: a wrong-signing SDK default, orphan attachments landing at repo root (aboutme.jpeg, webdevelopment.jpeg), a Decap media library that writes into git instead of S3, and no single robust place for images. We are **dropping Garage S3 for Kontra media entirely**. Images live as **files in the content repo, co-located with their article** — one source of truth, versioned, backed up, and served by the app. ## 1. Target layout (Option A — images with their article) ``` content/ └── subjects/ └── world/ ├── _subject.yaml ├── chinese-chips/ │ ├── chinese-chips.md ← article (same filename → same URL /articles/chinese-chips) │ ├── aboutme.jpeg ← images beside their article │ └── hero.jpg ├── southern-harbor/ │ ├── southern-harbor.md │ └── harbor.jpg └── river-border/ └── river-border.md ``` - **Slug is unchanged** (from filename) — URLs `/articles/` stay identical. - **Obsidian's native setting** ("attachments: same folder as current file") puts images beside the article automatically — no config drift, no stray files at repo root. - **Decap** reads/writes the same git tree, so it sees the same folders (its image widget becomes a string `{{media:...}}`; no library files committed). - **App** serves images at `/media/` by reading the repo folder — a plain file server. ## 2. Why Option A over a top-level media/ folder - Obsidian's default attachment behavior (same folder) needs **zero configuration** and cannot orphan files — the exact failure mode that created `aboutme.jpeg` at root. - Each article is a **self-contained folder** (text + images): move/copy/archive in one step. - Collisions impossible — filenames only clash within the same article. - Serving complexity is identical to a central media folder (both are static file serving). ## 3. Changes required (minimal, mechanical) 1. **Content repo** - Migrate articles to folders: `subjects///.md` (+ move their images in). - Move stray images (`aboutme.jpeg`, `webdevelopment.jpeg`) into their article folder. - Add `content/.gitignore`: nothing special (images are intentionally tracked now); but add `.trash/` to gitignore so Obsidian's trash stops entering git. - Add `/media`-relevant note in `_templates/article.md`. 2. **App loader** (`app/src/content.go`) - Make article discovery **recurse one level** under each subject folder (currently non-recursive: `*.md` at subject root, `_subject.yaml` skipped). New rule: - `subjects//.md` (flat, existing) **and** `subjects///*.md` (new). - Still skip `_`-prefixed files/dirs (config, templates). - Slug = filename without `.md`, unchanged. 3. **App media route** (`app/src/main.go` + handler) - Replace the Garage proxy: `/media/{path...}` → serve the file from `/subjects///` or from the repo root `media/` if any. Simplest robust: look up `/` in the content root (covers any location) with a safe-path guard (no `..`), set Content-Type from extension. - **Reference convention (DECIDED)**: full repo-relative path — `{{media:chinese-chips/aboutme.jpeg}}`. The app maps `/media/{path...}` → file relative to content root (safe-path guarded). - Remove `/admin/media` upload page + Garage upload endpoint + minio-go dependency (or keep minio-go for future — recommend removing to keep binary lean). 4. **Front-matter/{{media:}}** - `{{media:chinese-chips/aboutme.jpeg}}` → `/media/chinese-chips/aboutme.jpeg` → the app serves the file from the content root (safe-path guarded). Works for images in any folder (article folders, pages). - Obsidian/Decap reference images by the same repo-relative path: `{{media:/}}` for an image beside its article. 5. **Cleanup** - Delete `real2.png` (+ `photonic-chips.png` leftover) from bucket — irrelevant now (S3 dropped). - Remove the Garage bucket/key/kontra-day specific bits if unused elsewhere. - Update `PLAN.md`, `DESIGN.md` (media section), `README.md`, `docs/OVERVIEW.md` to the new model; remove S3/Decap-media claims. 6. **Backups** - Media now lives in the **content git repo** → backed up wherever the repo is: Gitea (.35) + production clone (.13) + the existing offsite/NAB backup chain. - No new backup config needed. ## 4. Migration steps (in order) 1. Commit a branch: move the 5 known articles into folders (+ images), update `.gitignore`. 2. Loosen the loader (change #2), build, verify all existing URLs return 200. 3. Swap the media route (change #3), build, verify `{{media:...}}` images serve from the repo. 4. Push → autosync pulls to .13 → rebuild container with new binary. 5. Remove the Decap media-library bits + S3 env from compose (or keep env unused). 6. Fix the test article's references; verify Obsidian + Decap + site all agree. 7. Update the docs (PLAN/DESIGN/README/OVERVIEW) and delete stale bucket artifacts. 8. Full end-to-end: edit image in Obsidian → push → live in ~30s; delete image → 404s. ## 5. Risks / notes - The loader change (recurse one level) is the only behavioral change to content discovery; existing flat articles still work, so rollback is one revert. - `/media/` file serving must guard against path traversal (`..`) — handled in change #3. - Obsidian trash: `.trash/` is already in the repo (Obsidian's own folder) — gitignoring it stops it syncing to Gitea while keeping local trash. - No secrets change; Gitea deploy token stays in `.env` (gitignored). ## 6. Out of scope (unless you ask) - Reintroducing any S3/Garage for Kontra media. - A browser-based media manager beyond the simple file page. - Multi-editor auth beyond the existing Gitea/Obsidian workflow.