Add issue No. 02 "Obsession" + reusable image pipeline

Content
- 100 new plates under content/posts/02/ — one recurring red-haired model
  across 10 editorial registers (Haute Couture, Film Noir, Boudoir,
  Avant-Garde, Minimalism, Baroque, Street Style, Surrealism, Monochrome,
  Nocturne), generated with flux-1.1-pro, face-swapped to a consistent
  identity (FaceFusion), and upscaled (Upscayl + Remacri, 3328x4992).
- content/issues/02/_index.md; issue 01 -> status archived; hugo.toml params
  (issueIds newest-first, issueNumber/Name/Season/Blurb).

Structure
- Plate bundles grouped by issue: content/posts/<issue>/<slug>/, with
  build.render:never stubs so /posts/<issue>/ isn't emitted. nginx 301s the
  legacy flat /posts/<slug>/ URLs to /posts/01/<slug>/. The /posts/ archive
  uses .RegularPagesRecursive.

Image pipeline (scripts/, data/ gitignored)
- generate-images.py  — Replicate flux-1.1-pro, 960x1440, idempotent.
- faceswap-images.py  — FaceFusion batch-run, one consistent face.
- upscale-images.py   — Upscayl + remacri-4x, long edge clamped to 4992.
- build-issue.py      — prompts JSON -> content/posts/<issue>/*/index.md.

Build
- CSS now compiled by Hugo's css.TailwindCSS (partials/css.html +
  templates.Defer); dropped the standalone Tailwind CLI step, concurrently,
  and the gitignored static/css/main.css. Requires Hugo >= 0.161; Dockerfile
  collapsed to a single hugomods/hugo:debian-node build stage.
  pnpm-workspace.yaml: preferSymlinkedExecutables + allowBuilds.

Front end
- Pagination 8 per page.
- Lightbox: brand mark and category link out; robust SPA back/forward
  (fetch before startViewTransition, single-flight guard, swallow the
  transition abort rejection, popstate re-renders / reopens the viewer).

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01DZPmxGywFnAhmYJB1eh9fm
This commit is contained in:
2026-08-29 21:11:24 +02:00
co-authored by Claude Sonnet 5
parent 1480f73a8f
commit 2dc7a0063f
432 changed files with 3195 additions and 830 deletions
+40 -18
View File
@@ -5,16 +5,17 @@ This file provides guidance to Claude Code (claude.ai/code) when working with co
## Commands
```bash
pnpm dev # Tailwind watch + Hugo dev server in parallel
pnpm build # build:css then hugo --minify → public/
pnpm build:css # Tailwind only → static/css/main.css
pnpm watch:css # Tailwind watch (CSS only)
hugo server -D # Hugo dev server (drafts included)
hugo --minify # Production build
python3 scripts/import-posts.py # Import posts from ~/projects/ginger/posts.csv
pnpm install # installs @tailwindcss/cli; Hugo shells out to it
pnpm dev # hugo server -D (Hugo compiles Tailwind + hot-reloads)
pnpm build # hugo --minify → public/
python3 scripts/import-posts.py # Import issue 01 from ~/projects/ginger/posts.csv → content/posts/01/
python3 scripts/generate-images.py --prompts data/prompts/issue-NN.json --issue NN # Replicate flux-1.1-pro → content/posts/NN/<slug>/<slug>.png
python3 scripts/faceswap-images.py --issue NN --source ~/Bilder/palina.webp # FaceFusion: swap one consistent face into every plate (in place)
python3 scripts/upscale-images.py --issue NN # Upscayl + Remacri 4x → clamp to 4992px long edge (in place)
python3 scripts/build-issue.py --issue NN --prompts data/prompts/issue-NN.json # write content/posts/NN/<slug>/index.md
```
CSS must be compiled before Hugo runs. `pnpm build` does both in order. In dev, `pnpm dev` runs both concurrently.
Hugo (≥ 0.161) compiles Tailwind itself during the build — no separate CSS step.
## Architecture
@@ -22,12 +23,26 @@ CSS must be compiled before Hugo runs. `pnpm build` does both in order. In dev,
### CSS pipeline
Tailwind reads `assets/css/main.css` and writes to `static/css/main.css` (gitignored — build artefact). The `@theme {}` block defines all design tokens (`--color-paper`, `--color-roux`, `--font-display`, etc.). Global resets are in `@layer base` so Tailwind utility classes correctly override them.
`layouts/partials/css.html` runs `resources.Get "css/main.css" | css.TailwindCSS`
inside a `templates.Defer` block (invoked from `head.html`). Hugo shells out to
`node_modules/.bin/tailwindcss`, which scans `hugo_stats.json` (written each build
by `[build.buildStats]`, `@source`'d in `main.css`, mounted to
`assets/notwatching/` and cache-busted per `hugo.toml`). Dev serves unminified
`/css/main.css`; production emits a fingerprinted, SRI-hashed file.
`assets/css/main.css``@theme {}` defines all design tokens (`--color-paper`,
`--color-roux`, `--font-display`, …); global resets are in `@layer base` so
Tailwind utilities override them.
`pnpm-workspace.yaml` sets `preferSymlinkedExecutables: true` (Hugo rejects pnpm's
default shell shim for `tailwindcss`) and `allowBuilds` for `@parcel/watcher` (so
`pnpm run` doesn't fail on the ignored native build).
### Content model
- `content/posts/<slug>/index.md` — page bundles; each has a `plate` number, `issues`, `categories` (single item), `tags`, `featured` flag, and a `.png` image resource.
- `content/issues/<id>/_index.md`branch bundle for an issue; metadata (`title`, `description`, `issueNumber`, `season`, `status`) drives the homepage hero and issue list page.
- `content/posts/<issue>/<slug>/index.md` — page bundles grouped by issue dir (`01/`, `02/`, …); each has a `plate` number, `issues`, `categories` (single item), `tags`, `featured` flag, and a `.png` image resource. Permalink is `/posts/<issue>/<slug>/`.
- `content/posts/<issue>/_index.md``build.render: never`; suppresses the intermediate `/posts/<issue>/` list page (the per-issue landing page is the taxonomy term `/issues/<issue>/`).
- `content/issues/<id>/_index.md` — branch bundle for an issue; metadata (`title`, `description`, `issueNumber`, `season`, `status`) drives the homepage hero and issue list page. Only one issue should have `status: "current"`.
- `content/imprint.md` — standalone page using `layout: imprint`.
Taxonomies: `categories`, `tags`, `issues` (defined in `hugo.toml`).
@@ -36,7 +51,7 @@ Taxonomies: `categories`, `tags`, `issues` (defined in `hugo.toml`).
```
layouts/baseof.html ← shell: head, header, main, footer, lightbox, roux-data JSON
layouts/index.html ← homepage: current-issue hero + 5 featured plates
layouts/index.html ← homepage: current-issue hero + up to 5 featured plates from that issue
layouts/_default/list.html ← two branches: kind="taxonomy" (term cards) | kind="term"/section (paginated post grid)
layouts/_default/single.html ← post plate page (triggers lightbox via window.__ROUX_OPEN_SLUG)
layouts/_default/imprint.html← prose page layout
@@ -45,7 +60,7 @@ layouts/issues/terms.html ← issue index, reads issueIds from hugo.toml para
layouts/404.html ← editorial 404 page
```
Key partials: `header.html`, `footer.html`, `card.html` (reusable post card), `lightbox.html`, `pagination.html`, `logo.html`.
Key partials: `head.html`, `css.html` (Tailwind build, see CSS pipeline), `header.html`, `footer.html`, `card.html` (reusable post card), `lightbox.html`, `pagination.html`, `logo.html`.
### JavaScript (`static/js/app.js`)
@@ -58,16 +73,23 @@ Responsibilities:
### Data flow: homepage featured plates
The homepage shows exactly the posts with `featured: true` in frontmatter. Mark posts via `featured: true`; the template filters with `where .Site.RegularPages ".Params.featured" true`.
The homepage hero resolves the current issue via `.Site.Params.issueNumber``GetPage "/issues/<num>"`. The featured grid shows up to 5 posts with `featured: true` **within that current issue** (`where $plates ".Params.featured" true | first 5`), so each issue's prompts file flags its own 5.
### Content import
### Content pipeline
`scripts/import-posts.py` reads `~/projects/ginger/posts.csv` and `~/projects/ginger/images/final/selected/`. It creates/updates page bundles deterministically (sorted by filename → stable plate numbers). Run it from the repo root after updating the upstream CSV or image set.
- `scripts/import-posts.py` — issue 01 only; reads `~/projects/ginger/posts.csv` + `~/projects/ginger/images/final/selected/`, writes `content/posts/01/` deterministically (sorted by filename → stable plate numbers).
- `scripts/generate-images.py` — reusable Replicate generator. `--prompts <json> --issue NN`; token from `$REPLICATE_API_TOKEN` or `~/.env`; default model `black-forest-labs/flux-1.1-pro`, 960×1440 PNG (custom aspect); idempotent (skips existing), writes a `<prompts>.generated.json` provenance manifest.
- `scripts/faceswap-images.py``--issue NN --source <face>` (default `~/Bilder/palina.webp`). Runs FaceFusion `batch-run` (via `~/Projekte/facefusion/.venv`) once over the whole issue, swapping in one consistent face; writes back in place. `--enhance` adds the face_enhancer pass. Idempotent via `issue-NN.faceswap.json` keyed on the source hash.
- `scripts/upscale-images.py``--issue NN`. Runs `upscayl-bin` (`/usr/share/upscayl/bin`) with the `remacri-4x` model once over the issue, then clamps the long edge to `--max-edge` (default 4992 ≈ issue-01 sources) with ImageMagick; writes back in place. Idempotent via `issue-NN.upscale.json`.
- `scripts/build-issue.py` — generic; reads the same prompts JSON, writes `content/posts/NN/<slug>/index.md` (plate numbers by stable slug sort), `featured` sourced from the JSON.
- `data/prompts/issue-NN.json` — array of `{slug, title, category, tags, description, prompt, featured?}`. `data/` is gitignored: the prompts file and the `*.generated/faceswap/upscale.json` manifests are local pipeline scratch, not committed. The plate bundles under `content/posts/NN/` are the tracked output.
Full pipeline order: `generate-images``faceswap-images``upscale-images``build-issue`. All four operate on `content/posts/NN/<slug>/<slug>.png` in place and are individually idempotent; pass `--out DIR` to any of the image scripts to stage results without touching the bundles (that run is then untracked).
### Hugo params (hugo.toml)
`issueNumber`, `issueName`, `issueSeason`, `issueBlurb` are fallback values used when `$.Site.GetPage "/issues/01"` returns nil. `issueIds` drives the issues terms page. `umamiSrc`/`umamiId` are injected only in production (`hugo.IsProduction`).
`issueNumber`, `issueName`, `issueSeason`, `issueBlurb` are fallback values used when `$.Site.GetPage "/issues/<issueNumber>"` returns nil. `issueIds` (newest-first) drives the issues terms page order. `umamiSrc`/`umamiId` are injected only in production (`hugo.IsProduction`).
### nginx
`nginx.conf` at repo root is the production server config (used in Docker). `error_page 404 /404.html` is already wired.
`nginx.conf` at repo root is the production server config (used in Docker). `error_page 404 /404.html` is already wired. A `location ~ ^/posts/(?!0[12]/)…` block 301-redirects the legacy flat issue-01 plate URLs (`/posts/<slug>/`) to their new home under `/posts/01/<slug>/`; extend the negative-lookahead when adding issue 03+.