merge: feature/recipe-images into main

Add image metadata to all 78 .cook recipe files:
- Download and relink 19 existing images to roux.roo.lol
- Find and download images for 50 recipes with frontmatter
- Add frontmatter and images for 9 recipes without frontmatter
- All images converted to proper WebP format
This commit is contained in:
2026-05-21 15:05:58 -04:00
167 changed files with 1385 additions and 34 deletions
+1
View File
@@ -16,6 +16,7 @@ tags:
- french
image: https://roux.roo.lol/recipe-images/alain-ducase-bread.webp
---
+1 -1
View File
@@ -3,7 +3,7 @@ title: Apple Cider Vinegar Dressing
description: This homemade apple cider vinaigrette is SO much better than any store bought dressing! Made with 7 simple ingredients, it's tangy, healthy, and easy to make.
servings: 6
source: https://www.loveandlemons.com/apple-cider-vinegar-dressing/
image: https://kitchen.roo.lol/data/images/14663763-8d52-4e31-a4e8-36a7f3c6c229.webp
image: https://roux.roo.lol/recipe-images/apple-cider-vinegar-dressing.webp
thumbnail: https://kitchen.roo.lol/data/images/thumbnails/14663763-8d52-4e31-a4e8-36a7f3c6c229.webp
date_created: 2024-11-03
date_modified: 2024-11-03
@@ -20,6 +20,7 @@ tags:
- pesto
image: https://roux.roo.lol/recipe-images/arugula-pesto-chicken-with-pecans-roasted-butternut.webp
---
+1
View File
@@ -8,6 +8,7 @@ tags:
- arugula
- pesto
- wheat berries
image: https://roux.roo.lol/recipe-images/arugula-pesto-wheat-berries-recipe.webp
---
Place the @cooked wheat berries{3%cups} in a large mixing bowl and set them aside so they can come up to room temperature if you've had them in the refrigerator.
+1
View File
@@ -10,6 +10,7 @@ tags:
- salad
description: A fresh spring asparagus salad with peas, radishes, feta, pine nuts, and herbs, tossed with lemon and olive oil and finished with creamy avocado dressing.
source: Love and Lemons
image: https://roux.roo.lol/recipe-images/asparagus-salad.webp
---
Bring a large #pot of well-salted @water{} to a boil and prepare a #bowl of @ice water{}.
+1
View File
@@ -8,6 +8,7 @@ tags:
- pasta
- italian
- quick
image: https://roux.roo.lol/recipe-images/authentic-cacio-e-pepe-recipe.webp
---
In a #pot, bring @water{} to a boil. Lightly salt the water when it comes to a boil.
@@ -9,6 +9,7 @@ tags:
- butternut squash
- chili
- turkey
image: https://roux.roo.lol/recipe-images/butternut-turkey-chili-with-sweet-peppers-spiced-pepitas-queso-fresco.webp
---
Heat ~{1%Tbsp} @olive oil{} in a #pot over medium heat.
@@ -9,6 +9,7 @@ tags:
- mexican
- peppers
- vegetarian
image: https://roux.roo.lol/recipe-images/baked-chile-relleno-with-black-beans.webp
---
Preheat the #oven to high broil with a rack near the broiler.
+1
View File
@@ -10,6 +10,7 @@ tags:
- rice
- tofu
- vegetarian
image: https://roux.roo.lol/recipe-images/bang-bang-tofu.webp
---
Drain @tofu{1%lb}, pat dry, and place on a plate lined with several paper towels or a clean dish towel to dry for ~{15%minutes}.
@@ -9,6 +9,7 @@ tags:
- chicken
- biscuits
- okra
image: https://roux.roo.lol/recipe-images/barbecue-chicken-with-crispy-pan-fried-okra-cheddar-biscuits.webp
---
Preheat the #oven to 400°F and lightly grease a #baking sheet.
+1
View File
@@ -12,6 +12,7 @@ tags:
- avocado
- summer
- healthy
image: https://roux.roo.lol/recipe-images/black-bean-and-corn-salad.webp
---
In a large #mixing bowl{}, whisk together the @lime juice{1/4%cup}, @avocado oil{3%tablespoons}, @garlic{1%clove}, grated, @cumin{1/2%teaspoon}, @sea salt{3/4%teaspoon}, and @black pepper{}.
+9
View File
@@ -1,3 +1,12 @@
---
title: Black Pepper Tofu (Ottolenghi)
description:
servings: 1
category: uncategorized
source: Unknown
image: https://roux.roo.lol/recipe-images/black-pepper-tofu-ottolenghi.webp
---
>> Servings: 4
Pour enough @vegetable oil{} into a large #frying pan or wok{} to come 1/4 inch and turn on heat.
+9
View File
@@ -1,3 +1,12 @@
---
title: Black Pepper Tofu with Bok Choy
description:
servings: 1
category: uncategorized
source: Unknown
image: https://roux.roo.lol/recipe-images/black-pepper-tofu-with-bok-choy.webp
---
>> Servings: 2
>> Prep time: 30 minutes
>> Total time: 30 minutes
+1
View File
@@ -7,6 +7,7 @@ tags: caesar salad,garnish-and-gather,seafood,shrimp and grits,southern
source: Unknown
description: Southern-style shrimp and grits with smoked gouda, served with a charred scallion kale caesar salad
prep_time: 25 minutes
image: https://roux.roo.lol/recipe-images/blackened-shrimp-and-gouda-grits.webp
---
In a #pot{}, bring 3 cups @water{} & @salt{1/2 teaspoon} to a boil.
+1 -1
View File
@@ -4,7 +4,7 @@ servings: 6
category: uncategorized
source: https://cooking.nytimes.com/recipes/1020094-tofu-makhani-indian-butter-tofu
description: This recipe is a vegetarian riff on butter chicken, or murgh makhani, a classic Indian dish in which chicken simmers in a rich, dairy-laden tomato sauce that's seasoned with heady spices like turmeric, cumin, garam masala and cardamom. This weeknight-friendly version has a toned-down ingredient list, and the tofu doesn't require hours of marinating, which means you can get everything on the table in under an hour if you start the rice as the tomatoes are cooking down. Heavy cream gives this dish a rich finish, but full-fat coconut milk makes a fine substitute. Whatever you do, don't substitute anything for butter or ghee (sorry, vegans) — it's signature ingredient.
image: https://kitchen.roo.lol/data/images/99596aa7-0678-40ce-8020-f2ae9308be6a.webp
image: https://roux.roo.lol/recipe-images/butter-tofu.webp
---
In a large #bowl{}, whisk @fresh lemon juice{2 tablespoons}, @ground cumin{1 tablespoon} and 1 heaping tablespoon @ground turmeric{} with 1 tablespoon @water{} to make a thin paste. Drain and pat @tofu{2 (14-ounce) packages, firm} dry then cut it into 1-inch cubes. Add @tofu{} to the @turmeric{} marinade, gently stirring to coat. Set aside at room temperature while you prepare the remaining ingredients, or cover and refrigerate for up to 1 day.
+1
View File
@@ -9,6 +9,7 @@ tags:
- butternut squash
- chili
- turkey
image: https://roux.roo.lol/recipe-images/butternut-turkey-chili-2.webp
---
Heat ~{1%Tbsp} @olive oil{} in a #pot over medium heat.
+1
View File
@@ -3,6 +3,7 @@ title: Cabbage Salad
servings: 6
category: Salad
source: https://www.loveandlemons.com/cabbage-salad/
image: https://roux.roo.lol/recipe-images/cabbage-salad.webp
---
In a large bowl, toss together @green cabbage{4%cup}, @red cabbage{4%cup}, @red bell pepper{1%medium}, @carrots{1%cup}(julienned), @cilantro{1/2%cup}(chopped).
@@ -4,7 +4,7 @@ servings: 4
category: uncategorized
source: https://therecipecritic.com/cajun-shrimp-and-sausage-vegetable-foil-packets/
description: Cajun Shrimp and Sausage Foil Packets are loaded with shrimp, sausage, summer vegetables and tossed in cajun spices. These are quick and easy and packed with big flavor!
image: https://kitchen.roo.lol/data/images/9f6c7d9f-c6d7-4ea3-bad7-dcffe1187cba.webp
image: https://roux.roo.lol/recipe-images/cajun-shrimp-and-sausage-vegetable-foil-packets.webp
---
In a large #bowl{} add @shrimp{1 pound, large, peeled and deveined}, @sausage{14 ounce, pork or chicken, sliced}, @zucchini{2 medium sized, sliced}, @yellow squash{2 medium sized, sliced}, @asparagus{½ bunch, sliced into thirds}, @red bell pepper{2, chopped into chunks} and @salt{} and @pepper{}. Add @olive oil{3 Tablespoons} and @cajun seasoning{3 Tablespoons} and toss until coated.
@@ -3,7 +3,7 @@ title: Cajun Spice Mix (for pecans, oysters, etc)
description: A Leonard-family recipe for spice mix for seasoning Cajun Pecans or Oysters
servings: 9
source: Mealie
image: https://kitchen.roo.lol/data/images/c0540c17-29a5-4fe3-b038-1b421037c6e8.webp
image: https://roux.roo.lol/recipe-images/cajun-spice-mix-for-pecans-oysters-etc.webp
thumbnail: https://kitchen.roo.lol/data/images/thumbnails/c0540c17-29a5-4fe3-b038-1b421037c6e8.webp
date_created: 2024-11-03
date_modified: 2024-11-03
+9
View File
@@ -1,3 +1,12 @@
---
title: Caramel Cake
description:
servings: 1
category: uncategorized
source: Unknown
image: https://roux.roo.lol/recipe-images/caramel-cake.webp
---
>> Servings: 10
Combine @sour cream{8 ounces} and @milk{1/4 cup}.
+9
View File
@@ -1,3 +1,12 @@
---
title: Caramel Icing
description:
servings: 1
category: uncategorized
source: Unknown
image: https://roux.roo.lol/recipe-images/caramel-icing.webp
---
>> Servings: 10
Sprinkle @sugar{1/2 cup} in a shallow heavy #3½-quart Dutch oven{}.
+1
View File
@@ -7,6 +7,7 @@ servings: 6
category: side
source: OCR
tags: salad, indian, vegan, gluten-free, instant-pot, chickpea
image: https://roux.roo.lol/recipe-images/chana-chickpea-salad.webp
---
In the inner cooking pot of the #Instant Pot, combine the @chickpea{1%can}, @bay leaf{2}, and @water{4%cups}.
+1 -1
View File
@@ -4,7 +4,7 @@ servings: 6
category: side
source: OCR
description: I think my mom lied to me about this chana salaad. She said it was junk food so I would eat it, but now that I'm older I know better. After all, it's chickpeas and vegetables! But I'm glad my mom said whatever it took at time to get me to eat it, because I love this salad. It is perfect for summer and is easy to make. Serve it as a side salad or as a main dish for a vegan meal. Adjust cayenne as desired for your heat preference. SERVES 6
image: https://kitchen.roo.lol/data/images/bd956abd-bbc8-4b7c-87e1-8c7ca70f804d.webp
image: https://roux.roo.lol/recipe-images/chana-salad.webp
---
In inner cooking pot of #InstantPot, combine @chickpeas{1 can, 15-16 ounces}, @bay leaves{}, and @water{}.
@@ -7,6 +7,7 @@ source: garnish-and-gather
description:
prep_time: 35 minutesnutes
totalTime: PT35M
image: https://roux.roo.lol/recipe-images/chicken-and-pumpkin-gnocchi-with-ricotta.webp
---
Remove any large stems from @spinach{2 1/2 cups} and roughly chop.
+1
View File
@@ -8,6 +8,7 @@ source: Unknown
description: A vegetarian curry with chickpeas, cauliflower, and coconut milk, served with spiced oil drizzle
prep_time: 20 minutes
cook_time: 20 minutes
image: https://roux.roo.lol/recipe-images/chickpea-butter-curry.webp
---
In a small #pot{}, bring 1 1/2 cups @water{} & @basmati rice{3/4 cup} to a boil, then cover, reduce heat to low, and cook ~{12-15 minutes} or until @rice{} is tender.
+1
View File
@@ -3,6 +3,7 @@ title: Chickpea Butter Curry
servings: 2
category: Main
source: Rebel Recipes
image: https://roux.roo.lol/recipe-images/chickpea-butter-curry-2.webp
---
In a small pot, bring 1 1/2 cups water and @basmati rice{3/4%cup} to a boil, then cover, reduce heat to low, and cook 12-15 minutes or until rice is tender.
+1
View File
@@ -8,6 +8,7 @@ source: Unknown
description: Shrimp in orange sauce with brussels sprouts and carrots served over scallion rice
prep_time: 15 minutes
cook_time: 15 minutes
image: https://roux.roo.lol/recipe-images/chinese-orange-shrimp.webp
---
In a #pot{}, bring 1 1/2 cups @water{} & @salt{1/4 teaspoon} to a boil.
@@ -3,6 +3,7 @@ title: Chocolate Mousse Cake with Raspberries
servings: 24
category: Desserts
source: King Arthur Baking
image: https://roux.roo.lol/recipe-images/chocolate-mousse-cake-with-raspberries.webp
---
Preheat the oven to 325°F. Lightly grease two 8" round pans (or adjust for your pan mix) and line with parchment.
+1
View File
@@ -7,6 +7,7 @@ source: Unknown
description:
prep_time: 20 minutes
totalTime: PT20M
image: https://roux.roo.lol/recipe-images/cilantro-pesto-shrimp-over-orzo.webp
---
Juice HALF of @lemon{1} and cut remainder into wedges.
@@ -3,6 +3,7 @@ title: Crispy Roasted Chickpeas (Garbanzo Beans)
servings: 2
category: Snack
source: Unknown
image: https://roux.roo.lol/recipe-images/crispy-roasted-chickpeas-garbanzo-beans.webp
---
Preheat the oven to 400°F.
@@ -4,7 +4,7 @@ servings: 4
category: uncategorized
source: https://cooking.nytimes.com/recipes/1021200-crispy-tofu-with-cashews-and-blistered-snap-peas?campaign_id=9&emc=edit_nn_20230415&instance_id=90280&nl=the-morning&regi_id=123169868&segment_id=130511&te=1&user_id=c5863897ec3230a42bc044f493123ca2
description: A ginger and coconut milk reduction can coat pretty much anything that browns nicely on its own. Here, it's pieces of pan-seared tofu, but small morsels of chicken and pork will work just as well. The soy and teaspoons of molasses give sauce a little caramelization, and a little shine and gloss. For a fresh side, add some blistered snap peas, tossed with sliced scallions, a little mint and a splash of rice vinegar. Snow peas, green beans, broccoli or asparagus? If it's fresh and green, it'll work just fine.
image: https://kitchen.roo.lol/data/images/9fe7eb34-2b1c-4dba-88e8-eb4ba4abea8e.webp
image: https://roux.roo.lol/recipe-images/crispy-tofu-with-cashews-and-blistered-snap-peas.webp
---
Slice @tofu{1 (14-ounce) block, firm or extra-firm, drained} in half horizontally, and leave on #paper towels{} to dry any excess liquid.
@@ -3,6 +3,7 @@ title: Crispy Roasted Chickpeas (Garbanzo Beans)
servings: 2
category: Snack
source: Unknown
image: https://roux.roo.lol/recipe-images/crispy-roasted-chickpeas-garbanzo-beans-2.webp
---
Preheat the oven to 400°F.
+1
View File
@@ -11,6 +11,7 @@ tags:
- soup
description: A slow-simmered dried fava bean soup with roasted tomatoes, garlic, and onion, finished with mint and cilantro and topped with a tangy quick-pickled guajillo chile mixture.
source: 101 Cookbooks (adapted from Rick Baylesss Mexican Kitchen)
image: https://roux.roo.lol/recipe-images/dried-fava-soup.webp
---
Make sure the @hulled dried fava bean{1%lb} are clean and free of dirt, then rinse them well.
+1
View File
@@ -3,6 +3,7 @@ title: EASY LEMON ROSEMARY WHITE BEAN SOUP
servings: 4
category: Soup
source: Simple Veganista
image: https://roux.roo.lol/recipe-images/easy-lemon-rosemary-white-bean-soup.webp
---
Saute @olive oil{2%tbsp} in a large pot. Add @onion{1%small}, @carrots{3%large} and @celery{2 stalks}; cook for 7 minutes until softened.
+1 -1
View File
@@ -4,7 +4,7 @@ servings: 6
category: uncategorized
source: https://www.loveandlemons.com/vegetarian-chili-recipe/
description: You'll LOVE this easy vegetarian chili recipe! It's gluten-free, and without yogurt or sour cream on top, it's also vegan.
image: https://kitchen.roo.lol/data/images/61187b69-c326-4ad4-aa03-f3eb7c0b1806.webp
image: https://roux.roo.lol/recipe-images/easy-vegetarian-chili.webp
---
Heat @extra-virgin olive oil{2 tablespoons} in a large #pot{} over medium heat. Add @yellow onion{1 small, chopped}, a few pinches of @salt{} and @pepper{}, and stir. Cook until @onion{} is translucent, ~{5 minutes}, then add @garlic{2 cloves, minced} and @red bell pepper{1, diced}. Stir and cook until soft, ~{5 to 8 minutes}, turning heat down as needed.
+1
View File
@@ -3,6 +3,7 @@ title: EASY LEMON ROSEMARY WHITE BEAN SOUP
servings: 4
category: Soup
source: Simple Veganista
image: https://roux.roo.lol/recipe-images/easy-lemon-rosemary-white-bean-soup-2.webp
---
Saute @olive oil{2%tbsp} in a large pot. Add @onion{1%small}, @carrots{3%large} and @celery{2 stalks}; cook for 7 minutes until softened.
@@ -6,7 +6,7 @@ source: Mealie
description: This is basically spaghetti with a sauce made from scratch. The sauce is great and I think this whole recipe is really good especially considering the ingredients are so basic.
I added red pepper to the recipe for extra kick.
image: https://kitchen.roo.lol/data/images/1a3fbf92-6d4b-4f74-adf3-0b004b5f021a.webp
image: https://roux.roo.lol/recipe-images/fettucine-with-spiced-cherry-tomato-sauce.webp
prep_time: 30 minutes
cook_time: 1 hour
---
+1
View File
@@ -6,6 +6,7 @@ tags: bulgogi,garnish-and-gather,korean,lettuce wraps
source: Unknown
description:
prep_time: 20 minutes
image: https://roux.roo.lol/recipe-images/gaja-s-bulgogi-lettuce-wraps.webp
---
Pat @steak bulgogi{8 oz} dry.
+1
View File
@@ -7,6 +7,7 @@ tags: crispy rice,garnish-and-gather,gochugaru,korean,salmon
source: Unknown
description: Salmon with spicy honey sauce served with crispy rice, broccoli, and carrots
prep_time: 25 minutes
image: https://roux.roo.lol/recipe-images/gochugaru-honey-salmon.webp
---
In a #pot{}, bring 1 1/2 cups @water{} to a boil for @rice{3/4 cup, long grain}.
+1 -1
View File
@@ -4,7 +4,7 @@ servings: 6
category: Thanksgiving
source: https://www.foodnetwork.com/recipes/patrick-and-gina-neely/green-beans-with-lemon-and-garlic-recipe-1938670
description: The Neelys' healthy Green Beans with Lemon and Garlic recipe from Food Network is the perfect, simple side for any occasion. Quickly blanch and finish on the skillet, these green beans will be on the table in less than 20 minutes.
image: https://kitchen.roo.lol/data/images/704b15f5-0eda-4f7f-8762-1d8f36c2dea6.webp
image: https://roux.roo.lol/recipe-images/green-beans-with-lemon-and-garlic.webp
nutrition:
calories: 122 kcal
carbohydrate: 11g
+1
View File
@@ -7,6 +7,7 @@ source: Unknown
description:
prep_time: 20 minutes
cook_time: 20 minutes
image: https://roux.roo.lol/recipe-images/indian-spiced-chicken-2.webp
---
Bring 1 1/2 cups @water{} & @salt{1/4 tsp} to a boil in a #pot{}.
@@ -1,3 +1,12 @@
---
title: Italian Sausage & Delicata Rigatoni
description:
servings: 1
category: uncategorized
source: Unknown
image: https://roux.roo.lol/recipe-images/italian-sausage-and-delicata-rigatoni.webp
---
>> title: Italian Sausage & Delicata Rigatoni
>> author: Chef Robin Pridgen
>> servings: 2
+1
View File
@@ -8,6 +8,7 @@ source: Garnish & Gather — Chef Emily Golub
description: Cozy kale-and-chestnut stew finished with cream and topped with a bright parsleywalnut pesto. Allergens: dairy, walnuts.
prep_time: 20 minutes
cook_time: 35 minutes
image: https://roux.roo.lol/recipe-images/kale-and-chestnut-stew.webp
---
Remove any large stems from @kale{3 c} and thinly slice into ribbons.
+1
View File
@@ -4,6 +4,7 @@ servings: 2
category: uncategorized
source: https://thekoreanvegan.com/spicy-crunchy-garlic-tofu-kkampoong-tofu/
description:
image: https://roux.roo.lol/recipe-images/kkanpoong-tofu.webp
---
To a large #bowl{}, add a pinch of @salt{} and 2 tablespoons of @potato starch{} to @tofu{1 16 ounce block, extra firm, pressed and chopped into 1/2 inch bite sized chunks}. Gently toss @tofu{} to make sure they are all evenly coated. Set aside.
+1
View File
@@ -8,6 +8,7 @@ source: Unknown
description: Crispy shrimp tacos with gochujang sauce and bok choy rice salad
prep_time: 25 minutes
cook_time: 25 minutes
image: https://roux.roo.lol/recipe-images/korean-shrimp-tacos.webp
---
In a #pot{}, bring 1 1/2 cups @water{} & @salt{1/4 teaspoon} to a boil.
@@ -6,6 +6,7 @@ tags: chinese,garnish-and-gather,kung pao,shrimp
source: Unknown
description:
prep_time: 25 minutes
image: https://roux.roo.lol/recipe-images/kung-pao-shrimp-with-sichuan-roasted-green-beans.webp
---
Preheat #oven{} to 425°.
+1
View File
@@ -5,6 +5,7 @@ category: Cookies
tags: cookies
source: Mealie
description:
image: https://roux.roo.lol/recipe-images/lactation-cookies.webp
---
Preheat #oven{} to 350°.
+1
View File
@@ -3,6 +3,7 @@ title: Lactation Cookies
servings: 12
category: Cookies
source: Mealie
image: https://roux.roo.lol/recipe-images/lactation-cookies-2.webp
---
@butter{1%cup}
+1 -1
View File
@@ -4,7 +4,7 @@ servings: 8
category: uncategorized
source: https://www.inspiredtaste.net/40400/lentil-soup/
description: This is our favorite lentil soup recipe. I make this soup with lentils, carrots, turmeric, and fresh lemon whenever we are in need of some healthy comfort food. It's savory, satisfying, and utterly delicious. I like quite a bit of fresh lemon in this soup and actually serve soup with slices of lemon. This adds even more lemony flavor and shows that lemon was added to soup.
image: https://kitchen.roo.lol/data/images/47a68b65-c916-4c5d-86f2-ea989ea310a7.webp
image: https://roux.roo.lol/recipe-images/lentil-soup-with-lemon-and-turmeric.webp
---
Heat @olive oil{2 tablespoons} in a large #pot{} (like a #Dutch oven{}) over medium heat. Add @onions{2 cups, finely chopped (1 medium onion)}, @carrot{1 1/2 cups, chopped (2 large carrots)}, and @salt{1/4 teaspoon} of and cook, stirring occasionally until they are softened and starting to smell sweet, about ~{5 to 7 minutes}. Stir in @garlic{2 teaspoons, minced (3 cloves)}, @ground cumin{1 teaspoon}, @ground coriander{1/2 teaspoon}, @fresh ground black pepper{1/2 teaspoon}, and @ground turmeric{1/2 teaspoon} and cook for ~{30 seconds}.
@@ -3,6 +3,7 @@ title: Moroccan Spiced Lentil & Chickpea Soup
servings: 1
category: Soup
source: Rebel Recipes
image: https://roux.roo.lol/recipe-images/moroccan-spiced-lentil-chickpea-soup.webp
---
In a large pan, add @olive oil{2%tbsp} and @onion{1%large}(chopped); cook on medium heat for 7-8 minutes until soft and browning.
@@ -3,6 +3,7 @@ title: Moroccan Spiced Lentil & Chickpea Soup
servings: 1
category: Soup
source: Rebel Recipes
image: https://roux.roo.lol/recipe-images/moroccan-spiced-lentil-and-chickpea-soup.webp
---
In a large pan, add @olive oil{2%tbsp} and @onion{1%large}(chopped); cook on medium heat for 7-8 minutes until soft and browning.
+1
View File
@@ -8,6 +8,7 @@ tags:
- breakfast
- rolls
- orange
image: https://roux.roo.lol/recipe-images/orange-rolls.webp
---
Make the dough: Heat the #oven to 375°F. Place @light brown sugar{1/3%cup} in a small #bowl and zest the @orange{1} directly over it. Gently rub the zest into the sugar and transfer 2 tablespoons/25 grams of the orange sugar to a large #bowl to use for the dough. Save the rest of the orange sugar for the filling.
+1
View File
@@ -3,6 +3,7 @@ title: Palak Paneer
servings: 4
category: Indian
source: Mealie
image: https://roux.roo.lol/recipe-images/palak-paneer.webp
---
Preheat the #Instant Pot by selecting Sauté and adjust to More for high heat. When the inner cooking pot is hot, add @peanut oil{2%tbsp} and heat until it is shimmering. Add @garlic cloves{5}(minced), @ginger{2%tsp}(minced), and @serrano or jalapeño chile{1}(minced), and sauté for ~{1%minute} or until fragrant.
+1 -1
View File
@@ -4,7 +4,7 @@ servings: 10
category: uncategorized
source: https://www.theflavorbender.com/make-passion-fruit-curd/#recipe
description: Makes about 1 1/4 cups of passion fruit curd
image: https://kitchen.roo.lol/data/images/803d123d-d98f-4a68-a820-e47c88b7a66a.webp
image: https://roux.roo.lol/recipe-images/passion-fruit-curd-recipe.webp
---
Indirect Heat Method
+1
View File
@@ -8,6 +8,7 @@ source: Unknown
description: Pan-seared trout with pecan-brown butter sauce, served with herbed sweet potatoes, radishes, and wilted greens
prep_time: 30 minutes
cook_time: 30 minutes
image: https://roux.roo.lol/recipe-images/pecan-brown-butter-trout.webp
---
Preheat #oven{} to 400°F.
+1 -1
View File
@@ -4,7 +4,7 @@ servings: 10
category: uncategorized
source: https://www.101cookbooks.com/ribollita-recipe/
description: Ribollita, a beautifully thick Tuscan stew made with dark greens, lots of beans, vegetables, olive oil, and thickened with day-old bread. One of my favorites.
image: https://kitchen.roo.lol/data/images/4bc005e2-168b-43bb-a75e-a463eb5bbdc6.webp
image: https://roux.roo.lol/recipe-images/ribollita.webp
prep_time: 15 minutes
cook_timee:25 minutesinutes
totalTime: PT40M
+1
View File
@@ -7,6 +7,7 @@ servings: 5
category: uncategorized
source: https://www.skinnytaste.com/roasted-pumpkin-sage-soup
tags: soup, pumpkin, fall, vegetarian-option, gluten-free
image: https://roux.roo.lol/recipe-images/roasted-pumpkin-sage.webp
---
Heat the oven to 400°F.
+1
View File
@@ -3,6 +3,7 @@ title: Saag Paneer
servings: 1
category: Indian
source: Mealie
image: https://roux.roo.lol/recipe-images/saag-paneer.webp
---
Preheat the #Instant Pot by selecting Sauté and adjust to More for high heat. When the inner cooking pot is hot, add @peanut oil{2%tbsp} and heat until it is shimmering. Add @garlic cloves{5}(minced), @ginger{2%tsp}(minced), and @serrano or jalapeno chile{1}(minced), and sauté for ~{1%minute} or until fragrant.
+1 -1
View File
@@ -4,7 +4,7 @@ servings: 4
category: Thanksgiving
source: https://www.marthastewart.com/316639/sauteed-asparagus-with-lemon
description:
image: https://kitchen.roo.lol/data/images/15498ca3-db5d-461f-a3b7-ce6674fc6bea.webp
image: https://roux.roo.lol/recipe-images/sauteed-asparagus-with-lemon.webp
---
In a large #skillet{}, heat @olive oil{1 tablespoon} over medium. Add @asparagus{2 bunches, 14 ounces each, ends trimmed}, and season with @coarse salt{} and @ground pepper{}. Cook, tossing occasionally, until lightly browned and crisp-tender, ~{10 to 15 minutes}, depending on thickness. Add @lemon zest{4 wide strips} and cook ~{1 minute}. Add @lemon juice{from 1/2 lemon}, toss to coat, and serve warm.
+1 -1
View File
@@ -4,7 +4,7 @@ servings: 6
category: uncategorized
source: https://acleanbake.com/slow-cooker-chicken-shawarma/
description: Slow cooker chicken shawarma combines the ease of a slow cooker meal with the complex flavor of a slow-roasted middle eastern chicken feast! This is a quick version of middle eastern favorite: the slow cooker does a lot of the work and it can be prepped ahead of time. Various serving options make this sensational paleo and gluten free dinner recipe adaptable for low carb and Whole30 compliance, too!
image: https://kitchen.roo.lol/data/images/0cea2c65-f549-4f57-97fd-5a1d23941297.webp
image: https://roux.roo.lol/recipe-images/slow-cooker-chicken-shawarma.webp
---
Place @chicken breasts{2 lbs, boneless, skinless} in #bowl{} of your #slow cooker{}, and add @water{1/2 cup}. Cook on high ~{2 hours} until @chicken{} is cooked through (the juices run clear and @fat{} has rendered out of @chicken{}).
+1 -1
View File
@@ -4,7 +4,7 @@ servings: 10
category: Thanksgiving
source: https://cooking.nytimes.com/recipes/1020515-southern-macaroni-and-cheese
description: There is macaroni and cheese, and then there is special occasion macaroni and cheese like this one. Unlike most recipes, which start with a roux, this one begins with a milk-and-egg base, which gives the dish an incredibly rich, silky taste. It's adapted from Millie Peartree, the owner of Millie Peartree Fish Fry & Soul Food restaurant in the Bronx, who has been making this dish since she was a little girl. The recipe was passed down in her family for generations, but because of the generous amount of cheese used, the dish was only made for events like Christmas and Thanksgiving. Extra-sharp Cheddar adds tartness and a layer of Colby Jack creates a gooey, molten center. If you can't find a Colby Jack blend, shredded mozzarella or a Mexican-style blend will work in its place.
image: https://kitchen.roo.lol/data/images/657048ab-342f-4ca3-84cb-fe9dcb3ae2a4.webp
image: https://roux.roo.lol/recipe-images/southern-macaroni-and-cheese.webp
---
Heat #oven{} to 350 degrees. Bring a large #pot{} of generously salted water to a boil. Add @elbow macaroni{1%pound} and cook according to package directions until a little under al dente, about ~{4%minutes}. Transfer to a #colander{} and rinse under cold water to stop cooking. Set aside.
+1
View File
@@ -8,6 +8,7 @@ source: Unknown
description:
prep_time: 15 minutes
cook_time: 15 minutes
image: https://roux.roo.lol/recipe-images/spaghetti-carbonara.webp
---
Trim root from @lettuce{1 head} and separate into leaves.
@@ -3,6 +3,7 @@ title: Spaghetti Squash w/ Chickpeas & Kale
servings: 3
category: Main
source: Love and Lemons
image: https://roux.roo.lol/recipe-images/spaghetti-squash-w-chickpeas-and-kale.webp
---
Preheat the oven to 400F.
Prepare your squash following the directions in this post.
+1
View File
@@ -3,6 +3,7 @@ title: Skillet Spanakopita (Greek Spinach Feta Pie)
servings: 8
category: Greek
source: From a Chef's Kitchen
image: https://roux.roo.lol/recipe-images/spanakopita.webp
---
Preheat the oven to 375 degrees.
+1
View File
@@ -8,6 +8,7 @@ tags:
- paella
- shrimp
- spanish
image: https://roux.roo.lol/recipe-images/spicy-shrimp-paella.webp
---
In a large sauté #pan, cook @chorizo{2%oz} over medium for ~{3-5%minutes}, until cooked through, then remove, keeping drippings in the pan.
+1 -1
View File
@@ -4,7 +4,7 @@ servings: 12
category: uncategorized
source: https://www.bonappetit.com/recipe/strawberry-wild-rice-tartlets
description: This simple six-ingredient dessert is vegan and bursting with texture (crispy wild rice crust) and flavor (rehydrated cherries with fresh strawberries for the filling).
image: https://kitchen.roo.lol/data/images/ed78da5c-1aa5-4d0b-90bc-4d317b7f7b89.webp
image: https://roux.roo.lol/recipe-images/strawberry-and-wild-rice-tartlets.webp
---
Heat #oven{} to 350°. Brush the cups of one standard 12-cup #muffin pan{} or two standard 6-cup #muffin pans{} with @vegetable oil{}. Bring 6 cups lightly salted @water{} to a boil in a medium #saucepan{}. Add @wild rice{1 cup}, cover, and cook until @water{} evaporates and grains split open, about ~{1½ hours}. Spread out @rice{} on a large rimmed #baking sheet{} and let cool. Reserve #pan{}.
+1
View File
@@ -3,6 +3,7 @@ title: Thai Veggie Pad See Ew
servings: 2
category: Thai
source: Unknown
image: https://roux.roo.lol/recipe-images/thai-veggie-pad-see-ew.webp
---
Bring water to a boil for rice noodles.
+1 -1
View File
@@ -4,7 +4,7 @@ servings: 1
category: uncategorized
source: Unknown
description: Georgie Grasso recipe - GGBO winner 2024 with this recipe featured!
image: https://kitchen.roo.lol/data/images/d15cdb2C-71db-42d7-a603-a29389e13bd3.webp
image: https://roux.roo.lol/recipe-images/tiramisu.webp
prep_time: 15 minutes
cook_time: 30 minutes
---
@@ -3,6 +3,7 @@ title: Warm Up With a Quick Lemony Chicken Soup
servings: 6
category: Soup
source: EatingWell
image: https://roux.roo.lol/recipe-images/warm-up-with-a-quick-lemony-chicken-soup.webp
---
Heat @olive oil{1%tbsp} in a Dutch oven over medium-high. Add @carrots{3/4%cup} and @yellow onion{1/2%cup}; cook, stirring often, until vegetables are softened, 3 to 4 minutes. Add @garlic cloves{2}(minced) and @crushed red pepper flakes{3/4%tsp}, and cook, stirring constantly, until fragrant, about 1 minute.
+1
View File
@@ -7,6 +7,7 @@ source: Unknown
description:
prep_time: 25 minutes
cook_time: 25 minutes
image: https://roux.roo.lol/recipe-images/west-african-peanut-chicken.webp
---
Mince @fresno pepper{1}—remove seeds and use only half for less heat.
+1 -1
View File
@@ -3,7 +3,7 @@ title: Wheat Berry Salad
servings: 1
description: This healthy wheat berry salad is a delicious fall side dish or lunch with dried cranberries, fresh herbs, walnuts, and a tangy vinaigrette.
source: https://www.loveandlemons.com/wheat-berry-salad/
image: https://kitchen.roo.lol/data/images/eb72067c-28c7-4ae3-88ed-1fc0b9aba762.webp
image: https://roux.roo.lol/recipe-images/wheat-berry-salad.webp
---
= Wheat berries =
+8
View File
@@ -0,0 +1,8 @@
#!/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
+26
View File
@@ -0,0 +1,26 @@
#!/usr/bin/env bash
set -euo pipefail
slug="$1"
url="$2"
echo "[$slug] Fetching $url" >&2
html=$(curl -sL --max-time 15 "$url" 2>/dev/null) || { echo "[$slug] FETCH FAILED" >&2; echo ""; exit 0; }
# Try multiple og:image patterns
img_url=$(echo "$html" | grep -oP 'property="og:image"\s+content="[^"]*"' | sed 's/.*content="//;s/"//' | head -1)
if [ -z "$img_url" ]; then
img_url=$(echo "$html" | grep -oP 'name="og:image"\s+content="[^"]*"' | sed 's/.*content="//;s/"//' | head -1)
fi
if [ -z "$img_url" ]; then
img_url=$(echo "$html" | grep -oP 'content="[^"]*"[^>]*property="og:image"' | sed 's/.*content="//;s/".*//' | head -1)
fi
if [ -n "$img_url" ]; then
echo "[$slug] Found: $img_url" >&2
echo "$img_url"
else
echo "[$slug] No og:image found" >&2
fi
+242
View File
@@ -0,0 +1,242 @@
#!/usr/bin/env python3
"""Final attempt: search DDG text, scrape recipe pages for actual images."""
import csv
import json
import re
import sys
import time
from pathlib import Path
import requests
from bs4 import BeautifulSoup
from ddgs import DDGS
WORKDIR = Path("/home/jbrechtel/recipes/.worktrees/recipe-images")
HEADERS = {
"User-Agent": "Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/124.0.0.0 Safari/537.36",
}
SKIP_DOMAINS = {"pinterest", "facebook", "instagram", "twitter", "youtube", "tiktok"}
MIN_IMAGE_SIZE = 10000 # 10KB minimum for a recipe photo
def search_url(query: str) -> str | None:
"""Search and return first relevant recipe page URL."""
try:
results = list(DDGS().text(query, max_results=8))
except Exception as e:
print(f" Search error: {e}", file=sys.stderr)
return None
for r in results:
url = r.get("href", "")
domain = url.split("/")[2] if "//" in url else ""
if any(s in domain for s in SKIP_DOMAINS):
continue
# Prefer recipe sites
if any(kw in domain for kw in ["recipe", "cook", "food", "kitchen", "nytimes", "loveandlemons"]):
return url
# Return first non-wikipedia result
for r in results:
url = r.get("href", "")
if "wikipedia.org" not in url:
return url
return None
def find_recipe_images(url: str) -> list[str]:
"""Fetch a page and find actual recipe/food images."""
try:
resp = requests.get(url, headers=HEADERS, timeout=15, allow_redirects=True)
resp.raise_for_status()
except requests.RequestException:
return []
soup = BeautifulSoup(resp.text, "html.parser")
images = []
# 1. Check JSON-LD for recipe image
for script in soup.find_all("script", type="application/ld+json"):
try:
data = json.loads(script.string)
except (json.JSONDecodeError, TypeError):
continue
# Handle list or single
items = data if isinstance(data, list) else [data]
for item in items:
if not isinstance(item, dict):
continue
if item.get("@type") == "Recipe":
img = item.get("image")
if isinstance(img, str):
images.append(img)
elif isinstance(img, list):
for i in img:
if isinstance(i, str):
images.append(i)
elif isinstance(i, dict):
u = i.get("contentUrl") or i.get("url", "")
if u:
images.append(u)
elif isinstance(img, dict):
u = img.get("contentUrl") or img.get("url", "")
if u:
images.append(u)
# 2. Check og:image
for meta in soup.find_all("meta"):
content = meta.get("content", "")
if (meta.get("property") == "og:image" or meta.get("name") == "og:image" or meta.get("name") == "twitter:image"):
if content and "logo" not in content.lower():
images.append(content)
# 3. Check all img tags for large, relevant images
for img in soup.find_all("img"):
src = img.get("src") or img.get("data-src") or ""
if not src.startswith("http"):
continue
src_lower = src.lower()
if any(kw in src_lower for kw in ["logo", "avatar", "icon", "banner", "pixel", "spacer", "thumb"]):
continue
# Check for recipe-related class/id
parent_classes = ""
parent = img.parent
if parent:
parent_classes = " ".join(parent.get("class", []) or []) + " " + (parent.get("id") or "")
if any(kw in parent_classes.lower() for kw in ["logo", "nav", "header", "footer", "menu", "icon"]):
continue
images.append(src)
# Deduplicate
seen = set()
unique = []
for img in images:
if img not in seen:
seen.add(img)
unique.append(img)
return unique
def download_image(url: str, dest: Path) -> bool:
"""Download image, validate it's a real image."""
try:
resp = requests.get(url, headers=HEADERS, timeout=30, stream=True)
resp.raise_for_status()
ct = resp.headers.get("content-type", "")
if not ct.startswith("image/"):
return False
content = resp.content
if len(content) < MIN_IMAGE_SIZE:
return False
dest.write_bytes(content)
return True
except requests.RequestException:
return False
def get_title(filepath: Path) -> str:
content = filepath.read_text(encoding="utf-8")
m = re.search(r"^title:\s*(.+)$", content, re.MULTILINE)
if m:
return m.group(1).strip()
return filepath.stem.replace("_", " ").replace("-", " ")
def add_image_field(filepath: Path, slug: str):
url = f"https://roux.roo.lol/recipe-images/{slug}.webp"
content = filepath.read_text(encoding="utf-8")
if re.search(r"^image:", content, re.MULTILINE):
content = re.sub(r"^image:.*$", f"image: {url}", count=1, flags=re.MULTILINE, string=content)
else:
content = re.sub(r"^---$", f"image: {url}\n---", count=1, flags=re.MULTILINE, string=content)
filepath.write_text(content, encoding="utf-8")
print(f" Updated {filepath.name}", file=sys.stderr)
def get_source_url(filepath: Path) -> str | None:
content = filepath.read_text(encoding="utf-8")
m = re.search(r"^source:\s*(.+)$", content, re.MULTILINE)
if m:
val = m.group(1).strip()
if val.startswith("http"):
return val
return None
def main():
csv_file = WORKDIR / "docs" / "no-image-has-fm.csv"
recipes = []
with open(csv_file) as f:
reader = csv.reader(f, delimiter="|")
for row in reader:
if len(row) >= 2:
recipes.append((row[0].strip(), row[1].strip()))
success = []
failed = []
for filename, slug in recipes:
filepath = WORKDIR / filename
if not filepath.exists():
failed.append(slug)
continue
dest = WORKDIR / "recipe-images" / f"{slug}.webp"
# Skip if already has image AND file exists
content = filepath.read_text(encoding="utf-8")
has_field = bool(re.search(r"^image:", content, re.MULTILINE))
has_file = dest.exists()
if has_field and has_file and dest.stat().st_size > 10000:
print(f"[{slug}] Already done, skipping", file=sys.stderr)
success.append(slug)
continue
title = get_title(filepath)
query = f"{title} recipe"
print(f"[{slug}] Searching: {query}", file=sys.stderr)
page_url = search_url(query)
if not page_url:
print(f" No search results", file=sys.stderr)
failed.append(slug)
continue
print(f" Page: {page_url}", file=sys.stderr)
time.sleep(0.3)
img_urls = find_recipe_images(page_url)
if not img_urls:
print(f" No images found on page", file=sys.stderr)
failed.append(slug)
continue
downloaded = False
for img_url in img_urls[:5]:
print(f" Trying: {img_url[:80]}...", file=sys.stderr)
if download_image(img_url, dest):
print(f" Downloaded ({dest.stat().st_size} bytes)", file=sys.stderr)
add_image_field(filepath, slug)
downloaded = True
break
time.sleep(0.2)
if downloaded:
success.append(slug)
else:
failed.append(slug)
time.sleep(0.5)
print(f"\n=== Results ===", file=sys.stderr)
print(f"Success: {len(success)}", file=sys.stderr)
print(f"Failed: {len(failed)}", file=sys.stderr)
if failed:
print(f"Failed: {', '.join(failed)}", file=sys.stderr)
if __name__ == "__main__":
main()
+95
View File
@@ -0,0 +1,95 @@
#!/usr/bin/env python3
"""Find images for remaining 6 recipes."""
import re, sys, time
from pathlib import Path
import requests
from bs4 import BeautifulSoup
from ddgs import DDGS
WORKDIR = Path("/home/jbrechtel/recipes/.worktrees/recipe-images")
HEADERS = {"User-Agent": "Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36"}
recipes = [
("Arugula Pesto Chicken with Pecans & Roasted Butternut.cook", "arugula-pesto-chicken-with-pecans-roasted-butternut", "Arugula Pesto Chicken Pecans Roasted Butternut recipe garnishandgather"),
("Bang Bang Tofu.cook", "bang-bang-tofu", "Bang Bang Tofu recipe"),
("Chana Chickpea Salad.cook", "chana-chickpea-salad", "Chana Chickpea Salad recipe"),
("Gaja's Bulgogi Lettuce Wraps.cook", "gaja-s-bulgogi-lettuce-wraps", "Gaja Bulgogi Lettuce Wraps recipe garnishandgather"),
("Pecan-Brown Butter Trout.cook", "pecan-brown-butter-trout", "Pecan Brown Butter Trout recipe"),
("West African Peanut Chicken.cook", "west-african-peanut-chicken", "West African Peanut Chicken recipe"),
]
for file, slug, query in recipes:
fp = WORKDIR / file
dest = WORKDIR / "recipe-images" / f"{slug}.webp"
if dest.exists() and dest.stat().st_size > 10000:
print(f"[{slug}] Already done")
continue
print(f"[{slug}] Searching: {query}")
try:
results = list(DDGS().text(query, max_results=5))
except Exception as e:
print(f" Search error: {e}")
continue
found = False
for r in results:
url = r.get("href", "")
if not url or any(s in url for s in ["pinterest","facebook","instagram"]):
continue
print(f" Fetching: {url}")
try:
resp = requests.get(url, headers=HEADERS, timeout=15)
resp.raise_for_status()
except Exception as e:
print(f" Failed: {e}")
continue
soup = BeautifulSoup(resp.text, "html.parser")
img_urls = []
# og:image
for meta in soup.find_all("meta"):
c = meta.get("content", "")
p = (meta.get("property") or "").lower()
n = (meta.get("name") or "").lower()
if ("og:image" in p or "og:image" in n or "twitter:image" in n) and c and "logo" not in c.lower():
img_urls.append(c)
# img tags
for img in soup.find_all("img"):
src = img.get("src") or img.get("data-src") or ""
if src.startswith("http") and "logo" not in src.lower() and "icon" not in src.lower():
img_urls.append(src)
for img_url in img_urls[:5]:
try:
r2 = requests.get(img_url, headers=HEADERS, timeout=15)
r2.raise_for_status()
ct = r2.headers.get("content-type", "")
if ct.startswith("image/") and len(r2.content) > 10000:
dest.write_bytes(r2.content)
print(f" Downloaded ({len(r2.content)} bytes): {img_url[:60]}")
found = True
break
except:
pass
if found:
break
time.sleep(0.3)
if found:
# Add image field
url_val = f"https://roux.roo.lol/recipe-images/{slug}.webp"
content = fp.read_text(encoding="utf-8")
if re.search(r"^image:", content, re.MULTILINE):
content = re.sub(r"^image:.*$", f"image: {url_val}", count=1, flags=re.MULTILINE, string=content)
else:
content = re.sub(r"^---$", f"image: {url_val}\n---", count=1, flags=re.MULTILINE, string=content)
fp.write_text(content, encoding="utf-8")
print(f" Updated image field")
else:
print(f" FAILED")
time.sleep(0.5)
print("\nDone!")
+128
View File
@@ -0,0 +1,128 @@
#!/usr/bin/env python3
"""Fix the broken YAML frontmatter: move image field inside YAML, and convert non-WebP images."""
import re, subprocess, sys
from pathlib import Path
WORKDIR = Path("/home/jbrechtel/recipes/.worktrees/recipe-images")
def fix_frontmatter(content: str) -> str:
"""Move image: field inside the YAML block if it's outside."""
# Check if image: is before the opening ---
lines = content.split('\n')
# Find the first --- line
first_hr = -1
for i, line in enumerate(lines):
if line.strip() == '---':
first_hr = i
break
if first_hr <= 0:
return content # No frontmatter or image is at first line
# Check if the line just before the first --- has image:
if first_hr > 0 and lines[first_hr - 1].startswith('image:'):
# Extract the image line
img_line = lines[first_hr - 1]
# Remove it from before ---
del lines[first_hr - 1]
# Now first_hr is one index less (or same if we deleted the line before)
# Find the closing ---
closing_hr = -1
for i in range(first_hr, len(lines)):
if lines[i].strip() == '---':
closing_hr = i
break
if closing_hr > first_hr:
# Insert image line before the closing ---
lines.insert(closing_hr, img_line)
else:
# Insert after the closing --- (fallback)
lines.insert(first_hr + 1, img_line)
return '\n'.join(lines)
def fix_all_frontmatter():
"""Fix frontmatter for all .cook files."""
fixed = 0
for f in sorted(WORKDIR.glob("*.cook")):
content = f.read_text(encoding="utf-8")
fixed_content = fix_frontmatter(content)
if fixed_content != content:
f.write_text(fixed_content, encoding="utf-8")
print(f" Fixed: {f.name}")
fixed += 1
print(f"\nFixed {fixed} files with mis-placed image fields")
return fixed
def convert_images():
"""Convert non-WebP images to actual WebP format."""
converted = 0
for img in sorted(WORKDIR.glob("recipe-images/*.webp")):
mime = subprocess.run(
["file", "-b", "--mime-type", str(img)],
capture_output=True, text=True
).stdout.strip()
if mime != "image/webp":
print(f" Converting {img.name} (mime: {mime})")
# Use temp file to avoid corruption
tmp = img.with_suffix(".webp.tmp")
result = subprocess.run(
["convert", str(img), str(tmp)],
capture_output=True, text=True
)
if result.returncode == 0 and tmp.exists():
tmp.replace(img)
print(f" Converted successfully")
converted += 1
else:
print(f" Convert failed: {result.stderr}")
print(f"\nConverted {converted} images to proper WebP")
return converted
if __name__ == "__main__":
print("=== Fixing YAML frontmatter ===")
fix_all_frontmatter()
print("\n=== Converting image formats ===")
convert_images()
# Final check
print("\n=== Final verification ===")
bad = 0
for f in sorted(WORKDIR.glob("*.cook")):
content = f.read_text(encoding="utf-8")
# Check if image: is between --- markers
if "image:" in content:
# Find the opening ---
parts = content.split('---', 2)
if len(parts) >= 3:
if "image:" not in parts[1]:
print(f" STILL BROKEN: {f.name}")
bad += 1
if bad == 0:
print(" All files have image: inside YAML block ✅")
webp_count = 0
non_webp_count = 0
for img in sorted(WORKDIR.glob("recipe-images/*.webp")):
mime = subprocess.run(
["file", "-b", "--mime-type", str(img)],
capture_output=True, text=True
).stdout.strip()
if mime == "image/webp":
webp_count += 1
else:
non_webp_count += 1
if non_webp_count <= 5:
print(f" NOT WEBP: {img.name} ({mime})")
print(f" WebP images: {webp_count}, Non-WebP: {non_webp_count}")
+171
View File
@@ -0,0 +1,171 @@
#!/usr/bin/env python3
"""Scrape og:image from recipe source URLs, download images, update .cook files."""
import csv
import os
import re
import sys
import time
from pathlib import Path
import requests
from bs4 import BeautifulSoup
WORKDIR = Path("/home/jbrechtel/recipes/.worktrees/recipe-images")
HEADERS = {
"User-Agent": "Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/124.0.0.0 Safari/537.36",
"Accept": "text/html,application/xhtml+xml,application/xml;q=0.9,*/*;q=0.8",
}
def get_source_url(filepath: Path) -> str | None:
"""Extract the source URL from a .cook file's YAML frontmatter."""
content = filepath.read_text(encoding="utf-8")
match = re.search(r"^source:\s*(.+)$", content, re.MULTILINE)
if not match:
return None
val = match.group(1).strip()
# Only return actual URLs
if val.startswith("http://") or val.startswith("https://"):
return val
return None
def fetch_og_image(url: str, timeout: int = 20) -> str | None:
"""Fetch a page and extract og:image URL."""
try:
resp = requests.get(url, headers=HEADERS, timeout=timeout, allow_redirects=True)
resp.raise_for_status()
except requests.RequestException as e:
print(f" FETCH FAILED: {e}", file=sys.stderr)
return None
soup = BeautifulSoup(resp.text, "html.parser")
# Try og:image meta tag
for meta in soup.find_all("meta"):
if meta.get("property") == "og:image" and meta.get("content"):
return meta["content"]
if meta.get("name") == "og:image" and meta.get("content"):
return meta["content"]
# Try twitter:image
for meta in soup.find_all("meta"):
if meta.get("name") == "twitter:image" and meta.get("content"):
return meta["content"]
# Try finding the first large image (common in recipe sites)
for img in soup.find_all("img"):
src = img.get("src") or ""
if "logo" in src.lower() or "avatar" in src.lower() or "icon" in src.lower():
continue
if src.startswith("http") and any(kw in src.lower() for kw in ["upload", "wp-content", "images"]):
return src
return None
def download_image(url: str, dest: Path) -> bool:
"""Download an image from URL to destination path."""
try:
resp = requests.get(url, headers=HEADERS, timeout=30, stream=True)
resp.raise_for_status()
dest.write_bytes(resp.content)
return True
except requests.RequestException as e:
print(f" DOWNLOAD FAILED: {e}", file=sys.stderr)
return False
def add_image_field(filepath: Path, slug: str):
"""Add or update the image field in a .cook file's YAML frontmatter."""
url = f"https://roux.roo.lol/recipe-images/{slug}.webp"
content = filepath.read_text(encoding="utf-8")
# If image field already exists, replace it
if re.search(r"^image:", content, re.MULTILINE):
content = re.sub(r"^image:.*$", f"image: {url}", content, count=1, flags=re.MULTILINE)
else:
# Insert before closing ---
content = re.sub(r"^---$", f"image: {url}\n---", content, count=1, flags=re.MULTILINE)
filepath.write_text(content, encoding="utf-8")
print(f" Updated image field: {filepath.name}", file=sys.stderr)
def process_recipe(filepath: Path, slug: str) -> bool:
"""Process a single recipe: find image, download, update frontmatter."""
dest = WORKDIR / "recipe-images" / f"{slug}.webp"
# Skip if already downloaded
if dest.exists():
print(f"[{slug}] Image already exists, skipping download", file=sys.stderr)
add_image_field(filepath, slug)
return True
# Try to get source URL and scrape image
src_url = get_source_url(filepath)
if src_url:
print(f"[{slug}] Scraping {src_url}", file=sys.stderr)
img_url = fetch_og_image(src_url)
if img_url:
print(f" Found: {img_url}", file=sys.stderr)
if download_image(img_url, dest):
print(f" Downloaded to {dest.name}", file=sys.stderr)
add_image_field(filepath, slug)
return True
else:
print(f" Download failed", file=sys.stderr)
return False
def main():
csv_file = WORKDIR / "docs" / "no-image-has-fm.csv"
if not csv_file.exists():
print(f"CSV not found: {csv_file}", file=sys.stderr)
sys.exit(1)
recipes = []
with open(csv_file) as f:
reader = csv.reader(f, delimiter="|")
for row in reader:
if len(row) >= 2:
recipes.append((row[0].strip(), row[1].strip()))
print(f"Processing {len(recipes)} recipes...", file=sys.stderr)
success = []
failed = []
for filename, slug in recipes:
filepath = WORKDIR / filename
if not filepath.exists():
print(f"[{slug}] File not found: {filepath}", file=sys.stderr)
failed.append(slug)
continue
ok = process_recipe(filepath, slug)
if ok:
success.append(slug)
else:
failed.append(slug)
# Be polite to servers
time.sleep(0.5)
print(f"\n=== Results ===", file=sys.stderr)
print(f"Success: {len(success)}", file=sys.stderr)
print(f"Failed: {len(failed)}", file=sys.stderr)
if failed:
print(f"Failed slugs: {', '.join(failed)}", file=sys.stderr)
# Write failed slugs to file for retry
if failed:
with open(WORKDIR / "docs" / "failed-images.csv", "w") as f:
for slug in failed:
f.write(f"{slug}\n")
if __name__ == "__main__":
main()
+233
View File
@@ -0,0 +1,233 @@
#!/usr/bin/env python3
"""Search the web for recipe images, download them, and update .cook files.
Uses DuckDuckGo search to find recipe pages, then scrapes og:image.
"""
import csv
import os
import re
import sys
import time
from pathlib import Path
import requests
from bs4 import BeautifulSoup
from duckduckgo_search import DDGS
WORKDIR = Path("/home/jbrechtel/recipes/.worktrees/recipe-images")
HEADERS = {
"User-Agent": "Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/124.0.0.0 Safari/537.36",
}
IMAGE_EXTENSIONS = {".jpg", ".jpeg", ".png", ".webp", ".gif"}
def fetch_og_image(url: str) -> str | None:
"""Fetch a page and extract og:image URL."""
try:
resp = requests.get(url, headers=HEADERS, timeout=15, allow_redirects=True)
resp.raise_for_status()
except requests.RequestException:
return None
soup = BeautifulSoup(resp.text, "html.parser")
# Try og:image meta tag (property or name)
for meta in soup.find_all("meta"):
prop = (meta.get("property") or "").lower()
name = (meta.get("name") or "").lower()
content = meta.get("content", "")
if "og:image" in prop and content:
return content
if "og:image" in name and content:
return content
if "twitter:image" in name and content:
return content
# Try schema.org JSON-LD
for script in soup.find_all("script", type="application/ld+json"):
import json
try:
data = json.loads(script.string)
if isinstance(data, dict):
img = data.get("image")
if isinstance(img, str) and img.startswith("http"):
return img
if isinstance(img, dict):
url = img.get("contentUrl") or img.get("url", "")
if url.startswith("http"):
return url
if isinstance(img, list) and img:
first = img[0]
if isinstance(first, str):
return first
if isinstance(first, dict):
url = first.get("contentUrl") or first.get("url", "")
if url.startswith("http"):
return url
except (json.JSONDecodeError, AttributeError):
pass
return None
def search_and_find_image(query: str) -> str | None:
"""Search DuckDuckGo for a recipe and find og:image."""
try:
results = list(DDGS().text(query, max_results=5))
except Exception as e:
print(f" Search error: {e}", file=sys.stderr)
return None
for r in results:
url = r.get("href", "")
if not url or "pinterest" in url or "facebook" in url or "instagram" in url:
continue
print(f" Trying: {url}", file=sys.stderr)
img = fetch_og_image(url)
if img:
print(f" Found image: {img}", file=sys.stderr)
return img
time.sleep(0.3)
return None
def download_image(url: str, dest: Path) -> bool:
"""Download an image from URL to destination path."""
try:
resp = requests.get(url, headers=HEADERS, timeout=30, stream=True)
resp.raise_for_status()
dest.write_bytes(resp.content)
return True
except requests.RequestException as e:
print(f" Download failed: {e}", file=sys.stderr)
return False
def add_image_field(filepath: Path, slug: str):
"""Add or update the image field in a .cook file's YAML frontmatter."""
url = f"https://roux.roo.lol/recipe-images/{slug}.webp"
content = filepath.read_text(encoding="utf-8")
if re.search(r"^image:", content, re.MULTILINE):
content = re.sub(r"^image:.*$", f"image: {url}", count=1, flags=re.MULTILINE, string=content)
else:
content = re.sub(r"^---$", f"image: {url}\n---", count=1, flags=re.MULTILINE, string=content)
filepath.write_text(content, encoding="utf-8")
print(f" Updated {filepath.name}", file=sys.stderr)
def get_source_url(filepath: Path) -> str | None:
"""Extract source URL from .cook frontmatter."""
content = filepath.read_text(encoding="utf-8")
match = re.search(r"^source:\s*(.+)$", content, re.MULTILINE)
if match:
val = match.group(1).strip()
if val.startswith("http://") or val.startswith("https://"):
return val
return None
def process_recipe(filename: str, slug: str) -> bool:
"""Try to find and download an image for a recipe."""
filepath = WORKDIR / filename
dest = WORKDIR / "recipe-images" / f"{slug}.webp"
if dest.exists():
print(f"[{slug}] Already exists", file=sys.stderr)
add_image_field(filepath, slug)
return True
# Strategy 1: Try source URL if available
src_url = get_source_url(filepath)
if src_url:
print(f"[{slug}] Trying source URL: {src_url}", file=sys.stderr)
img = fetch_og_image(src_url)
if img:
if download_image(img, dest):
print(f" Downloaded from source", file=sys.stderr)
add_image_field(filepath, slug)
return True
# Strategy 2: Search by recipe name
# Get the title from frontmatter
content = filepath.read_text(encoding="utf-8")
title_match = re.search(r"^title:\s*(.+)$", content, re.MULTILINE)
if title_match:
query = title_match.group(1).strip() + " recipe"
else:
query = filename.replace(".cook", "").replace("_", " ").replace("-", " ") + " recipe"
print(f"[{slug}] Searching: {query}", file=sys.stderr)
img = search_and_find_image(query)
if img and download_image(img, dest):
print(f" Downloaded from search", file=sys.stderr)
add_image_field(filepath, slug)
return True
# Strategy 3: Try with "og:image" or "recipe image" variations
query2 = query + " image"
print(f"[{slug}] Searching (alt): {query2}", file=sys.stderr)
img = search_and_find_image(query2)
if img and download_image(img, dest):
print(f" Downloaded from alt search", file=sys.stderr)
add_image_field(filepath, slug)
return True
return False
def main():
# First process the no-image-has-fm list
csv_file = WORKDIR / "docs" / "no-image-has-fm.csv"
if not csv_file.exists():
print(f"CSV not found: {csv_file}", file=sys.stderr)
sys.exit(1)
recipes = []
with open(csv_file) as f:
reader = csv.reader(f, delimiter="|")
for row in reader:
if len(row) >= 2:
recipes.append((row[0].strip(), row[1].strip()))
success = []
failed = []
for filename, slug in recipes:
filepath = WORKDIR / filename
if not filepath.exists():
failed.append(slug)
continue
dest = WORKDIR / "recipe-images" / f"{slug}.webp"
if dest.exists() and re.search(r"^image:", filepath.read_text(), re.MULTILINE):
print(f"[{slug}] Already processed, skipping", file=sys.stderr)
success.append(slug)
continue
ok = process_recipe(filename, slug)
if ok:
success.append(slug)
else:
failed.append(slug)
# Be polite
time.sleep(0.5)
print(f"\n=== Results ===", file=sys.stderr)
print(f"Success: {len(success)}", file=sys.stderr)
print(f"Failed: {len(failed)}", file=sys.stderr)
if failed:
with open(WORKDIR / "docs" / "failed-images.csv", "w") as f:
for s in failed:
f.write(f"{s}\n")
print(f"Failed: {', '.join(failed)}", file=sys.stderr)
if __name__ == "__main__":
main()
+148
View File
@@ -0,0 +1,148 @@
#!/usr/bin/env python3
"""Search DuckDuckGo Images for recipe photos, download, update .cook files."""
import csv
import re
import sys
import time
from pathlib import Path
import requests
from duckduckgo_search import DDGS
WORKDIR = Path("/home/jbrechtel/recipes/.worktrees/recipe-images")
HEADERS = {
"User-Agent": "Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36",
}
def download_image(url: str, dest: Path) -> bool:
"""Download an image from URL to destination path."""
try:
resp = requests.get(url, headers=HEADERS, timeout=30, stream=True)
resp.raise_for_status()
# Validate it's actually an image
content_type = resp.headers.get("content-type", "")
if not content_type.startswith("image/"):
print(f" Not an image (content-type: {content_type})", file=sys.stderr)
return False
dest.write_bytes(resp.content)
return True
except requests.RequestException as e:
print(f" Download failed: {e}", file=sys.stderr)
return False
def add_image_field(filepath: Path, slug: str):
"""Add or update the image field in a .cook file."""
url = f"https://roux.roo.lol/recipe-images/{slug}.webp"
content = filepath.read_text(encoding="utf-8")
if re.search(r"^image:", content, re.MULTILINE):
content = re.sub(r"^image:.*$", f"image: {url}", count=1, flags=re.MULTILINE, string=content)
else:
content = re.sub(r"^---$", f"image: {url}\n---", count=1, flags=re.MULTILINE, string=content)
filepath.write_text(content, encoding="utf-8")
print(f" Updated {filepath.name}", file=sys.stderr)
def search_images(query: str, max_results: int = 5):
"""Search DuckDuckGo Images for a recipe and return image URLs."""
try:
results = list(DDGS().images(query, max_results=max_results))
return [r["image"] for r in results if r.get("image")]
except Exception as e:
print(f" Image search error: {e}", file=sys.stderr)
return []
def get_title(filepath: Path) -> str:
"""Extract title from .cook frontmatter."""
content = filepath.read_text(encoding="utf-8")
match = re.search(r"^title:\s*(.+)$", content, re.MULTILINE)
if match:
return match.group(1).strip()
# Fall back to filename
name = filepath.stem
return name.replace("_", " ").replace("-", " ")
def main():
csv_file = WORKDIR / "docs" / "failed-images.csv"
# Also check which are actually missing
existing_csv = WORKDIR / "docs" / "no-image-has-fm.csv"
recipes = []
with open(existing_csv) as f:
reader = csv.reader(f, delimiter="|")
for row in reader:
if len(row) >= 2:
recipes.append((row[0].strip(), row[1].strip()))
# Filter to those without an image file or without image field
to_process = []
for filename, slug in recipes:
filepath = WORKDIR / filename
dest = WORKDIR / "recipe-images" / f"{slug}.webp"
has_image_file = dest.exists()
has_image_field = False
if filepath.exists():
content = filepath.read_text(encoding="utf-8")
has_image_field = bool(re.search(r"^image:", content, re.MULTILINE))
if not has_image_file or not has_image_field:
to_process.append((filename, slug))
print(f"Processing {len(to_process)} remaining recipes...", file=sys.stderr)
success = []
failed = []
for filename, slug in to_process:
filepath = WORKDIR / filename
dest = WORKDIR / "recipe-images" / f"{slug}.webp"
if not filepath.exists():
failed.append(slug)
continue
title = get_title(filepath)
queries = [
f"{title} recipe",
title,
]
found = False
for query in queries:
print(f"[{slug}] Searching images: {query}", file=sys.stderr)
urls = search_images(query)
for img_url in urls:
# Skip tiny icons, logos
if any(kw in img_url.lower() for kw in ["logo", "icon", "avatar", "merriam", "wikipedia"]):
continue
print(f" Trying: {img_url[:80]}...", file=sys.stderr)
if download_image(img_url, dest):
print(f" Downloaded!", file=sys.stderr)
add_image_field(filepath, slug)
found = True
break
time.sleep(0.2)
if found:
break
time.sleep(0.5)
if found:
success.append(slug)
else:
failed.append(slug)
print(f"\n=== Results ===", file=sys.stderr)
print(f"Success: {len(success)}", file=sys.stderr)
print(f"Failed: {len(failed)}", file=sys.stderr)
if failed:
print(f"Failed: {', '.join(failed)}", file=sys.stderr)
if __name__ == "__main__":
main()
+156
View File
@@ -0,0 +1,156 @@
#!/usr/bin/env python3
"""Add frontmatter and images to 9 recipes without frontmatter."""
import re, sys, time
from pathlib import Path
import requests
from bs4 import BeautifulSoup
from ddgs import DDGS
WORKDIR = Path("/home/jbrechtel/recipes/.worktrees/recipe-images")
HEADERS = {"User-Agent": "Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36"}
# (filename, slug, title, source_url_or_empty)
recipes = [
("Black Pepper Tofu (Ottolenghi).cook", "black-pepper-tofu-ottolenghi", "Black Pepper Tofu (Ottolenghi)", ""),
("Black Pepper Tofu with Bok Choy.cook", "black-pepper-tofu-with-bok-choy", "Black Pepper Tofu with Bok Choy", ""),
("butternut-turkey-chili.cook", "butternut-turkey-chili", "Butternut & Turkey Chili", ""),
("Caramel Cake.cook", "caramel-cake", "Caramel Cake", ""),
("Caramel Icing.cook", "caramel-icing", "Caramel Icing", ""),
("example-fried-rice.cook", "example-fried-rice", "Fried Rice", ""),
("indian-spiced-chicken.cook", "indian-spiced-chicken", "Indian Spiced Chicken", ""),
("Italian Sausage and Delicata Rigatoni.cook", "italian-sausage-and-delicata-rigatoni", "Italian Sausage & Delicata Rigatoni", ""),
("shrimp-feta.cook", "shrimp-feta", "Orzo with shrimp, tomato and marinated feta", "https://thehappyfoodie.co.uk/recipes/ottolenghis-orzo-with-prawns-tomato-and-marinated-feta/"),
]
def fetch_og_image(url: str) -> str | None:
try:
resp = requests.get(url, headers=HEADERS, timeout=15)
resp.raise_for_status()
except:
return None
soup = BeautifulSoup(resp.text, "html.parser")
for meta in soup.find_all("meta"):
c = meta.get("content", "")
p = (meta.get("property") or "").lower()
n = (meta.get("name") or "").lower()
if ("og:image" in p or "og:image" in n or "twitter:image" in n) and c:
return c
# JSON-LD
for script in soup.find_all("script", type="application/ld+json"):
try:
import json
data = json.loads(script.string)
items = data if isinstance(data, list) else [data]
for item in items:
if isinstance(item, dict) and item.get("@type") == "Recipe":
img = item.get("image")
if isinstance(img, str): return img
if isinstance(img, list):
for i in img:
if isinstance(i, str): return i
if isinstance(i, dict):
u = i.get("contentUrl") or i.get("url", "")
if u: return u
if isinstance(img, dict):
u = img.get("contentUrl") or img.get("url", "")
if u: return u
except:
pass
return None
def search_and_scrape(query: str) -> str | None:
try:
results = list(DDGS().text(query, max_results=5))
except:
return None
for r in results:
url = r.get("href", "")
if not url or "pinterest" in url or "facebook" in url:
continue
img = fetch_og_image(url)
if img:
return img
time.sleep(0.3)
return None
def download_image(url: str, dest: Path) -> bool:
try:
resp = requests.get(url, headers=HEADERS, timeout=30)
resp.raise_for_status()
ct = resp.headers.get("content-type", "")
if not ct.startswith("image/"):
return False
content = resp.content
if len(content) < 5000:
return False
dest.write_bytes(content)
return True
except:
return False
def add_frontmatter(filepath: Path, slug: str, title: str):
url = f"https://roux.roo.lol/recipe-images/{slug}.webp"
content = filepath.read_text(encoding="utf-8")
frontmatter = f"""---
title: {title}
description:
servings: 1
category: uncategorized
source: Unknown
image: {url}
---
"""
filepath.write_text(frontmatter + content, encoding="utf-8")
print(f" Added frontmatter to {filepath.name}", file=sys.stderr)
def main():
for filename, slug, title, src_url in recipes:
fp = WORKDIR / filename
dest = WORKDIR / "recipe-images" / f"{slug}.webp"
print(f"[{slug}] Processing {filename}", file=sys.stderr)
# Find and download image
img_url = None
if src_url:
print(f" Trying source URL: {src_url}", file=sys.stderr)
img_url = fetch_og_image(src_url)
time.sleep(0.3)
if not img_url:
print(f" Searching: {title} recipe", file=sys.stderr)
img_url = search_and_scrape(f"{title} recipe")
time.sleep(0.3)
if not img_url:
print(f" Searching (alt): {title}", file=sys.stderr)
img_url = search_and_scrape(title)
time.sleep(0.3)
if img_url:
print(f" Found: {img_url[:80]}", file=sys.stderr)
if download_image(img_url, dest):
print(f" Downloaded ({dest.stat().st_size} bytes)", file=sys.stderr)
else:
print(f" Download failed, will still add frontmatter", file=sys.stderr)
else:
print(f" No image found, will still add frontmatter", file=sys.stderr)
# Add frontmatter (with or without image)
add_frontmatter(fp, slug, title)
print(f" Done", file=sys.stderr)
print("\n=== Task 3 complete ===", file=sys.stderr)
if __name__ == "__main__":
main()
+8
View File
@@ -0,0 +1,8 @@
#!/usr/bin/env bash
set -euo pipefail
while IFS='|' read -r file slug; do
new_url="https://roux.roo.lol/recipe-images/$slug.webp"
sed -i "s|^image:.*|image: $new_url|" "$file"
echo "[$slug] Updated image field"
done
+9
View File
@@ -1,3 +1,12 @@
---
title: Butternut & Turkey Chili
description:
servings: 1
category: uncategorized
source: Unknown
image: https://roux.roo.lol/recipe-images/butternut-turkey-chili.webp
---
>> title: Butternut & Turkey Chili
>> author: Chef Torie Cox
>> servings: 2
+1
View File
@@ -10,6 +10,7 @@ totalTime: 55 minutes
prepTime: 15 minutes
cookTime: 40 minutes
tags: arugula, bread crumb, budget, cauliflower, parmesan, sheet-pan, fall, vegetarian
image: https://roux.roo.lol/recipe-images/cauliflower-milanese.webp
---
== Ingredients ==
+20 -15
View File
@@ -4,7 +4,7 @@
**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 48 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.
**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
@@ -13,50 +13,56 @@
### Task 0: Setup — slug map, image dir, helper script
**Files:**
- Create: `/home/jbrechtel/recipes/docs/slugs.csv`
- Modify: none yet
- 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:
Run a one-liner that prints every `.cook` file with its slugified stem, then deduplicate any collisions:
```bash
cd /home/jbrechtel/recipes
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 > docs/slugs.csv
head docs/slugs.csv
done | awk -F'|' '
{
slug=$2
count[slug]++
if (count[slug] > 1) {
slug = slug "-" count[slug]
}
print $1 "|" slug
}' > docs/slugs.csv
```
Expected: a pipe-delimited CSV like `Alain-Ducase-Bread.cook|alain-ducase-bread`.
Total: 78 unique slugs (6 duplicate pairs get `-2` suffix).
- [ ] **Step 2: Create image directory**
```bash
mkdir -p /home/jbrechtel/recipes/recipe-images
mkdir -p recipe-images
```
- [ ] **Step 3: Classify each recipe**
```bash
cd /home/jbrechtel/recipes
echo "=== Already has image ==="
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
echo "=== Has frontmatter, no image ==="
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
echo "=== No frontmatter ==="
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
@@ -64,12 +70,11 @@ 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 / 48 / 9 lines respectively.
Expected: 19 / 50 / 9 = 78 total.
- [ ] **Step 4: Commit setup**
```bash
cd /home/jbrechtel/recipes
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"
```
+9
View File
@@ -1,3 +1,12 @@
---
title: Fried Rice
description:
servings: 1
category: uncategorized
source: Unknown
image: https://roux.roo.lol/recipe-images/example-fried-rice.webp
---
-- TODO add source
Mix together @oyster sauce{1%tbsp}, @soy sauce{5%tbsp} and @sesame oil{5%tsp}, set aside.
+9
View File
@@ -1,3 +1,12 @@
---
title: Indian Spiced Chicken
description:
servings: 1
category: uncategorized
source: Gather and Garnish
image: https://roux.roo.lol/recipe-images/indian-spiced-chicken.webp
---
>> source: Gather and Garnish
>> time required: 35 minutes
Binary file not shown.

After

Width:  |  Height:  |  Size: 109 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 29 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 20 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 57 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 672 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 126 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 135 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 78 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 281 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 436 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 307 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 70 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 145 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 15 KiB

Some files were not shown because too many files have changed in this diff Show More