6.1 KiB
6.1 KiB
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/<slug>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/<path>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.jpegat 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)
- Content repo
- Migrate articles to folders:
subjects/<s>/<slug>/<slug>.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.
- Migrate articles to folders:
- App loader (
app/src/content.go)- Make article discovery recurse one level under each subject folder (currently
non-recursive:
*.mdat subject root,_subject.yamlskipped). New rule:subjects/<s>/<slug>.md(flat, existing) andsubjects/<s>/<slug>/*.md(new).- Still skip
_-prefixed files/dirs (config, templates).
- Slug = filename without
.md, unchanged.
- Make article discovery recurse one level under each subject folder (currently
non-recursive:
- App media route (
app/src/main.go+ handler)- Replace the Garage proxy:
/media/{path...}→ serve the file from<content>/subjects/<s>/<slug>/<path>or from the repo rootmedia/if any. Simplest robust: look up<content>/<path>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/mediaupload page + Garage upload endpoint + minio-go dependency (or keep minio-go for future — recommend removing to keep binary lean).
- Replace the Garage proxy:
- 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:<slug>/<filename>}}for an image beside its article.
- Cleanup
- Delete
real2.png(+photonic-chips.pngleftover) 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.mdto the new model; remove S3/Decap-media claims.
- Delete
- 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)
- Commit a branch: move the 5 known articles into folders (+ images), update
.gitignore. - Loosen the loader (change #2), build, verify all existing URLs return 200.
- Swap the media route (change #3), build, verify
{{media:...}}images serve from the repo. - Push → autosync pulls to .13 → rebuild container with new binary.
- Remove the Decap media-library bits + S3 env from compose (or keep env unused).
- Fix the test article's references; verify Obsidian + Decap + site all agree.
- Update the docs (PLAN/DESIGN/README/OVERVIEW) and delete stale bucket artifacts.
- 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/<path>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.