docs: media model = git files beside articles (Garage S3 dropped); update README/PLAN/maps
This commit is contained in:
2
PLAN.md
2
PLAN.md
@@ -13,7 +13,7 @@ A news-opinion website ("Kontra Day") with an admin panel. Pages are mainly text
|
|||||||
| **Admin** | **Decap CMS** (at `admin.kontra.day`) | Free, open-source, browser-based UI that edits the same `.md` files directly and commits to git. No custom admin to build/maintain. |
|
| **Admin** | **Decap CMS** (at `admin.kontra.day`) | Free, open-source, browser-based UI that edits the same `.md` files directly and commits to git. No custom admin to build/maintain. |
|
||||||
| **Content source of truth** | **Git repo on Gitea (.35)** | History, rollback, multi-editor sync, off-site backup path. Matches your Gitea/Obsidian habits. |
|
| **Content source of truth** | **Git repo on Gitea (.35)** | History, rollback, multi-editor sync, off-site backup path. Matches your Gitea/Obsidian habits. |
|
||||||
| **Templates** | **templ components** chosen per page (front-matter `template:`) | See §6 — page-level → subject-level → site-level → theme fallback. |
|
| **Templates** | **templ components** chosen per page (front-matter `template:`) | See §6 — page-level → subject-level → site-level → theme fallback. |
|
||||||
| **Media** | **Garage S3 (on .13)** referenced via `{{media:...}}` shortcodes | Rename/migrate media without touching every article. |
|
| **Media** | **Git files beside each article** (`{{media:<slug>/<file>}}`) — Garage S3 dropped 2026-09-10 | Robust, versioned, backed up with content; no object-store auth/sync. |
|
||||||
| **Colours** | **CSS design tokens** (CSS custom properties), admin-editable values, not code | Per-subject accent colours as data, no template edits. |
|
| **Colours** | **CSS design tokens** (CSS custom properties), admin-editable values, not code | Per-subject accent colours as data, no template edits. |
|
||||||
| **Deploy** | **Git push → systemd timer `git pull` every 30s on .13** (no build step; Go renders on request) | Instant content updates; .27 can be offline. Timer chosen over webhook (fewer moving parts). |
|
| **Deploy** | **Git push → systemd timer `git pull` every 30s on .13** (no build step; Go renders on request) | Instant content updates; .27 can be offline. Timer chosen over webhook (fewer moving parts). |
|
||||||
| **Email phase (V)** | Revisit later — Go mail libs or hand off to network tooling | Defer until requirements are real. |
|
| **Email phase (V)** | Revisit later — Go mail libs or hand off to network tooling | Defer until requirements are real. |
|
||||||
|
|||||||
12
README.md
12
README.md
@@ -28,12 +28,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 -->|"record: media"| 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
|
||||||
|
|
||||||
@@ -57,7 +57,7 @@ flowchart LR
|
|||||||
| **Git is the content API** | Content has full history, rollback, branch review, and network-agnostic sync. No lock-in. |
|
| **Git is the content API** | Content has full history, rollback, branch review, and network-agnostic sync. No lock-in. |
|
||||||
| **Automatic publish pipeline** | `git push` → 30 s later the site is live. We prove it end-to-end (add, change, delete). |
|
| **Automatic publish pipeline** | `git push` → 30 s later the site is live. We prove it end-to-end (add, change, delete). |
|
||||||
| **Single-binary + embedded assets** | CSS, fonts, and the admin UI are compiled into the binary (`//go:embed`). Deploy = copy one file. |
|
| **Single-binary + embedded assets** | CSS, fonts, and the admin UI are compiled into the binary (`//go:embed`). Deploy = copy one file. |
|
||||||
| **Self-hosted, privacy-first** | Gitea, Garage S3, Caddy, Decap all run on own network infra — no third-party SaaS for content or media. |
|
| **Self-hosted, privacy-first** | Gitea + Caddy on own infra; media + content both live in git — no third-party SaaS, no object store to babysit. |
|
||||||
|
|
||||||
## The stack (GOTH)
|
## The stack (GOTH)
|
||||||
|
|
||||||
@@ -68,8 +68,8 @@ flowchart LR
|
|||||||
| **Interactivity** | htmx (server-rendered, no JS framework) | Progressive enhancement, SEO-safe |
|
| **Interactivity** | htmx (server-rendered, no JS framework) | Progressive enhancement, SEO-safe |
|
||||||
| **Styling** | Tailwind-style **design tokens** (CSS custom properties) | Themeable via `DESIGN.md`, per-subject accent colours as data, not code |
|
| **Styling** | Tailwind-style **design tokens** (CSS custom properties) | Themeable via `DESIGN.md`, per-subject accent colours as data, not code |
|
||||||
| **Markdown** | goldmark (CommonMark 0.31) | Content + front-matter (`title`, `author`, `date`, `subject`, `kicker`, `featured`…) |
|
| **Markdown** | goldmark (CommonMark 0.31) | Content + front-matter (`title`, `author`, `date`, `subject`, `kicker`, `featured`…) |
|
||||||
| **Admin** | Decap CMS | Browser editor over the same git repo; Gitea OAuth login (PKCE) |
|
| **Admin** | Decap CMS + Obsidian | Both edit the same git repo; Obsidian for daily writing + media drop-in |
|
||||||
| **Media** | Garage S3 | `{{media:…}}` / `{{embed:vimeo:…}}` shortcodes resolved at render |
|
| **Media** | git files | Images live beside their article in the repo; `{{media:<slug>/<file>}}` shortcodes resolved at render |
|
||||||
| **Git** | Gitea | Origin of truth; `sam/kontra-content` |
|
| **Git** | Gitea | Origin of truth; `sam/kontra-content` |
|
||||||
| **Proxy** | Caddy | TLS termination, hostname routing |
|
| **Proxy** | Caddy | TLS termination, hostname routing |
|
||||||
| **Deploy** | Docker + pull-loop | `git pull` every 30s; restart server on change |
|
| **Deploy** | Docker + pull-loop | `git pull` every 30s; restart server on change |
|
||||||
|
|||||||
110
docs/MEDIA-PLAN.md
Normal file
110
docs/MEDIA-PLAN.md
Normal file
@@ -0,0 +1,110 @@
|
|||||||
|
# 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.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/<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`.
|
||||||
|
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/<s>/<slug>.md` (flat, existing) **and** `subjects/<s>/<slug>/*.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
|
||||||
|
`<content>/subjects/<s>/<slug>/<path>` or from the repo root `media/` 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/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:<slug>/<filename>}}` 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/<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.
|
||||||
@@ -45,11 +45,11 @@ flowchart LR
|
|||||||
end
|
end
|
||||||
|
|
||||||
subgraph media["Media (Garage S3 on .13)"]
|
subgraph media["Media (Garage S3 on .13)"]
|
||||||
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 -->|"shortcode: media"| 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
|
||||||
|
|
||||||
|
|||||||
@@ -28,11 +28,11 @@ flowchart LR
|
|||||||
end
|
end
|
||||||
|
|
||||||
subgraph media["Media (Garage S3 on .13)"]
|
subgraph media["Media (Garage S3 on .13)"]
|
||||||
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 -->|"shortcode: media"| 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
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user