Files
recipes/docs/plans/2026-05-21-recipe-images-plan.md

12 KiB

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/<slug>.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:

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
mkdir -p recipe-images
  • Step 3: Classify each recipe
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
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"

Files:

  • Modify: each of the 19 .cook files listed in docs/has-image.csv

  • Create: recipe-images/<slug>.webp for each

  • Step 1: Write batch relink script

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
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
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:

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
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
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/<slug>.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
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/<batch-file>, 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: <slug>|<image-url> or <slug>|NOT FOUND if no good image is found."

Each subagent outputs to docs/batch-no-image-<xx>-results.txt.

Run 6 subagents in parallel (or sequentially if parallelism is limited).

  • Step 3: Download all discovered images

Collect all results and download:

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
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:

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
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
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/<slug>.webp for each

  • Step 1: Inspect each no-frontmatter recipe to determine a title

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:

---
title: <detected title>
description: 
servings: 1
category: uncategorized
source: Unknown
image: https://roux.roo.lol/recipe-images/<slug>.webp
---

Using a script:

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.cookCaramel Cake
  • example-fried-rice.cookFried Rice
  • shrimp-feta.cookShrimp Feta etc.
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
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
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
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
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
cd /home/jbrechtel/recipes
git add -A
git commit -m "chore: cleanup temp files after recipe image ingestion"