# Recipe Image Ingestion Implementation Plan > **For agentic workers:** REQUIRED SUB-SKILL: Use superpowers:subagent-driven-development (recommended) or superpowers:executing-plans to implement this plan task-by-task. Steps use checkbox (`- [ ]`) syntax for tracking. **Goal:** Give every `.cook` recipe file an `image:` field in YAML frontmatter pointing to `https://roux.roo.lol/recipe-images/.webp`, and download each image locally. **Architecture:** Three parallel batches — (1) relink the 19 existing images, (2) discover + download + link 50 missing images (recipes with frontmatter), (3) create frontmatter + discover + link 9 more (recipes without frontmatter). Use subagent parallel dispatch for image discovery to keep wall-clock time manageable. **Tech Stack:** bash, curl, ImageMagick (convert), subagent researcher for web image search --- ### Task 0: Setup — slug map, image dir, helper script **Files:** - Create: `docs/slugs.csv` - Create: `docs/has-image.csv` - Create: `docs/no-image-has-fm.csv` - Create: `docs/no-fm.csv` - [ ] **Step 1: Generate slug mapping** Run a one-liner that prints every `.cook` file with its slugified stem, then deduplicate any collisions: ```bash cd /home/jbrechtel/recipes/.worktrees/recipe-images for f in *.cook; do stem="${f%.cook}" slug=$(echo "$stem" \ | tr '[:upper:]' '[:lower:]' \ | sed -e 's/[^a-z0-9]/-/g' -e 's/--*/-/g' -e 's/^-//' -e 's/-$//') echo "$f|$slug" done | awk -F'|' ' { slug=$2 count[slug]++ if (count[slug] > 1) { slug = slug "-" count[slug] } print $1 "|" slug }' > docs/slugs.csv ``` Total: 78 unique slugs (6 duplicate pairs get `-2` suffix). - [ ] **Step 2: Create image directory** ```bash mkdir -p recipe-images ``` - [ ] **Step 3: Classify each recipe** ```bash cd /home/jbrechtel/recipes/.worktrees/recipe-images while IFS='|' read -r f slug; do if grep -q "^image:" "$f" 2>/dev/null; then echo "$f|$slug"; fi done < docs/slugs.csv > docs/has-image.csv while IFS='|' read -r f slug; do if head -1 "$f" 2>/dev/null | grep -q "^---"; then if ! grep -q "^image:" "$f" 2>/dev/null; then echo "$f|$slug"; fi fi done < docs/slugs.csv > docs/no-image-has-fm.csv while IFS='|' read -r f slug; do if ! head -1 "$f" 2>/dev/null | grep -q "^---"; then echo "$f|$slug"; fi done < docs/slugs.csv > docs/no-fm.csv wc -l docs/has-image.csv docs/no-image-has-fm.csv docs/no-fm.csv ``` Expected: 19 / 50 / 9 = 78 total. - [ ] **Step 4: Commit setup** ```bash git add docs/slugs.csv docs/has-image.csv docs/no-image-has-fm.csv docs/no-fm.csv git commit -m "chore: add recipe slug mapping and classification CSVs" ``` --- ### Task 1: Download & relink existing images (19 recipes) **Files:** - Modify: each of the 19 `.cook` files listed in `docs/has-image.csv` - Create: `recipe-images/.webp` for each - [ ] **Step 1: Write batch relink script** ```bash cd /home/jbrechtel/recipes cat > bin/batch-relink.sh << 'SCRIPT' #!/usr/bin/env bash set -euo pipefail while IFS='|' read -r file slug; do url=$(grep "^image:" "$file" | sed 's/^image:[[:space:]]*//') echo "[$slug] Downloading from $url" curl -sL -o "recipe-images/$slug.webp" "$url" || echo "[$slug] DOWNLOAD FAILED" done SCRIPT chmod +x bin/batch-relink.sh ``` - [ ] **Step 2: Run relink batch** ```bash cd /home/jbrechtel/recipes bash bin/batch-relink.sh < docs/has-image.csv ls recipe-images/ | wc -l ``` Expected: 19 `.webp` files in `recipe-images/`. - [ ] **Step 3: Verify all downloads succeeded** ```bash cd /home/jbrechtel/recipes while IFS='|' read -r file slug; do if [ ! -f "recipe-images/$slug.webp" ]; then echo "MISSING: $slug.webp" fi done < docs/has-image.csv ``` Expected: no output (all present). - [ ] **Step 4: Update image fields in the 19 .cook files** Use a script to replace each existing `image:` line with the new URL: ```bash cd /home/jbrechtel/recipes cat > bin/update-image-field.sh << 'SCRIPT' #!/usr/bin/env bash set -euo pipefail while IFS='|' read -r file slug; do new_url="https://roux.roo.lol/recipe-images/$slug.webp" # Replace the image line in-place using sed sed -i "s|^image:.*|image: $new_url|" "$file" echo "[$slug] Updated image field" done SCRIPT chmod +x bin/update-image-field.sh bash bin/update-image-field.sh < docs/has-image.csv ``` - [ ] **Step 5: Verify the updates** ```bash cd /home/jbrechtel/recipes while IFS='|' read -r file slug; do expected="https://roux.roo.lol/recipe-images/$slug.webp" actual=$(grep "^image:" "$file" | sed 's/^image:[[:space:]]*//') if [ "$actual" != "$expected" ]; then echo "MISMATCH: $file — expected $expected, got $actual" fi done < docs/has-image.csv ``` Expected: no output (all match). - [ ] **Step 6: Commit** ```bash cd /home/jbrechtel/recipes git add recipe-images/ bin/ git commit -m "feat: download and relink existing recipe images to roux.roo.lol" ``` --- ### Task 2: Find & download images for 48 recipes with frontmatter but no image **Files:** - Modify: each of the 48 `.cook` files listed in `docs/no-image-has-fm.csv` - Create: `recipe-images/.webp` for each Approach: Use parallel subagent dispatches, each subagent handling a small batch of recipes. The subagent will search the web for an appropriate image URL for each recipe, then the parent session downloads and links. - [ ] **Step 1: Split the 48 recipes into manageable batches** ```bash cd /home/jbrechtel/recipes split -l 8 docs/no-image-has-fm.csv docs/batch-no-image- ls docs/batch-no-image-* ``` Expected: 6 files (48 ÷ 8 = 6 batches). - [ ] **Step 2: Dispatch parallel subagents for image discovery** For each batch file, dispatch a researcher subagent with task: > "For each recipe filename and slug in `docs/`, search the web to find ONE publicly accessible image URL of the finished dish that I can download with curl. Return the result as pipe-delimited lines: `|` or `|NOT FOUND` if no good image is found." Each subagent outputs to `docs/batch-no-image--results.txt`. Run 6 subagents in parallel (or sequentially if parallelism is limited). - [ ] **Step 3: Download all discovered images** Collect all results and download: ```bash cd /home/jbrechtel/recipes cat docs/batch-no-image-*-results.txt | while IFS='|' read -r slug url; do if [ "$url" != "NOT FOUND" ] && [ -n "$url" ]; then echo "[$slug] Downloading $url" curl -sL -o "recipe-images/$slug.webp" "$url" || echo "[$slug] DOWNLOAD FAILED" else echo "[$slug] SKIPPED — no image found" fi done ``` - [ ] **Step 4: Convert any non-WebP images** ```bash cd /home/jbrechtel/recipes for img in recipe-images/*; do mime=$(file --mime-type -b "$img") if [ "$mime" != "image/webp" ]; then convert "$img" "$img" 2>/dev/null || true fi done ``` (ImageMagick's `convert` re-encodes to match extension. The `file` check is advisory; `convert` handles format detection.) - [ ] **Step 5: Add image field to each .cook file** For each line in `docs/no-image-has-fm.csv`: ```bash cd /home/jbrechtel/recipes cat > bin/add-image-field.sh << 'SCRIPT' #!/usr/bin/env bash set -euo pipefail while IFS='|' read -r file slug; do # Only proceed if an image was actually downloaded if [ -f "recipe-images/$slug.webp" ]; then new_url="https://roux.roo.lol/recipe-images/$slug.webp" # Insert image: line after the opening --- or after the last metadata field # Find the line number of the closing --- and insert before it awk -v url="$new_url" ' /^---/ && !seen { seen=1; print; next } seen && /^---/ { print "image: " url; print; exit } { print } ' "$file" > "${file}.tmp" && mv "${file}.tmp" "$file" echo "[$slug] Added image field" else echo "[$slug] SKIPPED — no downloaded image" fi done SCRIPT chmod +x bin/add-image-field.sh bash bin/add-image-field.sh < docs/no-image-has-fm.csv ``` - [ ] **Step 6: Verify** ```bash cd /home/jbrechtel/recipes while IFS='|' read -r file slug; do expected="https://roux.roo.lol/recipe-images/$slug.webp" actual=$(grep "^image:" "$file" | sed 's/^image:[[:space:]]*//') if [ "$actual" != "$expected" ]; then echo "MISSING or MISMATCH: $file" fi done < docs/no-image-has-fm.csv ``` Expected: no output (all match). - [ ] **Step 7: Commit** ```bash cd /home/jbrechtel/recipes git add recipe-images/ bin/ docs/batch-no-image-*-results.txt git commit -m "feat: add images to 48 recipes with frontmatter" ``` --- ### Task 3: Add frontmatter + images for 9 recipes without frontmatter **Files:** - Modify: each of the 9 `.cook` files listed in `docs/no-fm.csv` - Create: `recipe-images/.webp` for each - [ ] **Step 1: Inspect each no-frontmatter recipe to determine a title** ```bash cd /home/jbrechtel/recipes while IFS='|' read -r file slug; do echo "=== $file ===" head -5 "$file" echo "" done < docs/no-fm.csv ``` For each, determine a reasonable title. Use the filename stem (de-slugified) as the default title, or the first substantive line of the recipe. - [ ] **Step 2: Dispatch subagent to find images for these 9 recipes** Similar to Task 2 Step 2, but for the smaller batch. Either one batch of 9 or use the same split approach. - [ ] **Step 3: Download images** Same as Task 2 Step 3. - [ ] **Step 4: Add YAML frontmatter + image field to each file** For each file, prepend: ```yaml --- title: description: servings: 1 category: uncategorized source: Unknown image: https://roux.roo.lol/recipe-images/.webp --- ``` Using a script: ```bash cd /home/jbrechtel/recipes cat > bin/add-frontmatter.sh << 'SCRIPT' #!/usr/bin/env bash set -euo pipefail # Input: pipe-delimited file|slug|title while IFS='|' read -r file slug title; do if [ ! -f "recipe-images/$slug.webp" ]; then echo "[$slug] SKIPPED — no image downloaded" continue fi url="https://roux.roo.lol/recipe-images/$slug.webp" cat > "${file}.tmp" << EOF --- title: $title description: servings: 1 category: uncategorized source: Unknown image: $url --- $(cat "$file") EOF mv "${file}.tmp" "$file" echo "[$slug] Added frontmatter" done SCRIPT chmod +x bin/add-frontmatter.sh ``` - [ ] **Step 5: Run the frontmatter addition** First create the input with detected titles. The title for each should be extracted from the recipe content or derived from the filename. For example: - `Caramel Cake.cook` → `Caramel Cake` - `example-fried-rice.cook` → `Fried Rice` - `shrimp-feta.cook` → `Shrimp Feta` etc. ```bash cd /home/jbrechtel/recipes # Create a pipe-delimited file: filename|slug|title while IFS='|' read -r file slug; do # Extract a reasonable title from the recipe content # Use the filename stem as a fallback stem="${file%.cook}" title="${stem//_/ }" title="${title//-/ }" echo "$file|$slug|$title" done < docs/no-fm.csv > docs/no-fm-with-titles.csv bash bin/add-frontmatter.sh < docs/no-fm-with-titles.csv ``` - [ ] **Step 6: Verify** ```bash cd /home/jbrechtel/recipes while IFS='|' read -r file slug _; do if ! head -1 "$file" | grep -q "^---"; then echo "NO FRONTMATTER: $file" elif ! grep -q "^image:" "$file"; then echo "NO IMAGE: $file" fi done < docs/no-fm.csv ``` Expected: no output (all have frontmatter and image). - [ ] **Step 7: Commit** ```bash cd /home/jbrechtel/recipes git add recipe-images/ bin/ docs/no-fm-with-titles.csv git commit -m "feat: add frontmatter and images to 9 recipes without frontmatter" ``` --- ### Task 4: Verification & cleanup - [ ] **Step 1: Final full audit** ```bash cd /home/jbrechtel/recipes all_ok=true for f in *.cook; do stem="${f%.cook}" slug=$(echo "$stem" \ | tr '[:upper:]' '[:lower:]' \ | sed -e 's/[^a-z0-9]/-/g' -e 's/--*/-/g' -e 's/^-//' -e 's/-$//') expected="https://roux.roo.lol/recipe-images/$slug.webp" if ! grep -q "^image: $expected" "$f"; then echo "FAIL: $f — missing or wrong image field" all_ok=false fi if [ ! -f "recipe-images/$slug.webp" ]; then echo "FAIL: recipe-images/$slug.webp does not exist" all_ok=false fi done if $all_ok; then echo "ALL 78 RECIPES VERIFIED OK" fi ``` - [ ] **Step 2: Clean up temp files** ```bash cd /home/jbrechtel/recipes rm -f docs/batch-no-image-* docs/slugs.csv docs/has-image.csv docs/no-image-has-fm.csv docs/no-fm.csv docs/no-fm-with-titles.csv ``` - [ ] **Step 3: Final commit** ```bash cd /home/jbrechtel/recipes git add -A git commit -m "chore: cleanup temp files after recipe image ingestion" ```