Artificial Intelligence 9 min read 1889 words

Twelve years of recipes and a pantry nobody could search

ES
Twelve years of recipes and a pantry nobody could search

Migue is Miguel Ángel López Pascual, a cook from Seville and a good friend. Since 2014 he has been publishing traditional Andalusian recipes at lachozademigue.com: sirloin in whisky sauce, spinach with chickpeas, salmorejo, croquettes. Twelve years later that adds up to 1,388 posts, written by hand, one at a time, at roughly two per week.

The content was never the problem. The content is excellent and it is his. The problem was that the archive had become impossible to search: a default WordPress theme, Live Composer running the front page, 91 categories with a broken hierarchy and 775 tags. If you wanted to find out what to do with the dried ñora peppers left in your cupboard, there was no way in.

I offered to help, and took the chance to combine two things I had been wanting to try together: Claude Code for the redesign and LM Studio running locally for the heavy lifting on the content. Here is what came out of it.

Looking came before designing

Before touching a line of CSS I ran a full count of all 1,388 posts through the REST API. The numbers changed the whole design:

FindingConsequence
Flat paragraphs, no H2/H3, no listsThe design rests almost entirely on body typography
Only 13 featured images out of 1,388Depending on photography is forbidden in listings
91 categories and 775 tags, broken hierarchyNavigation cannot be a category menu
Twelve years of archive, ~2 posts/weekThis is a searchable cookbook, not a chronological blog

That second figure is the one that rules everything. Thirteen featured images for 1,388 recipes means any modern food-blog design — a grid of cards with big photos, a photographic hero, all of it — was simply impossible. The material isn’t there.

So the concept went the other way: the kitchen notebook. Bone-white paper, ink, typography with character, and one rule that orders everything else: a missing photo is not a gap, it is the design. Cards without an image sit in the same grid as those with one, carrying the same visual weight.

It is the same lesson I took from redesigning this blog a few days ago: the design comes out of the content you have, not the content you wish you had.

From loose paragraphs to actual recipes

A recipe in Migue’s WordPress looked like this: an introductory paragraph, one paragraph with every ingredient in running text (500 g of anchovies, 200 ml of vinegar…) and several unnumbered paragraphs of method. Perfect for a reader. Opaque for a search engine, or for anything trying to understand the recipe: no Recipe, no recipeIngredient, nothing.

Migrating 1,388 posts to structured blocks with Recipe schema is the classic job that inspires dread, because one mass mistake wrecks twelve years of somebody else’s writing. So the process was built on three rules:

  1. Never write without a report first. The report analyses and proposes; it never touches the database.
  2. The applier runs a dry run by default. It only writes if you explicitly pass en-serio.
  3. A copy of every post before touching it. Fully reversible, recipe by recipe.

With that in place, the migration ran in batches of 50, reviewing each batch’s CSV before applying it. The report classified every post as propuesta, revisar, sin_reconocer or no_es_receta, and anything flagged revisar came with a concrete reason: “Very few ingredients (2)”, “One ingredient is suspiciously long”.

1,358 ended up migrated. The four the parser could not read were done by hand, which is exactly how it should go: the machine handles 99.7% and you handle the rest.

The experiment: extracting ingredients with a local model

Here is the part I actually wanted to test.

The idea was to make the archive navigable by ingredient: land on /ingrediente/comino/ and see every recipe using cumin. In WordPress that means a custom taxonomy — a vocabulary with its own URL, archive page and counter, just like categories but with your own words.

The hard part is filling it. You have to read 1,388 ingredient lists written by hand over twelve years and pull the clean ingredient out of every line: 2 cloves of garlic gives garlic, 500 g of ripe tomato gives tomato. That is 11,111 lines.

I could have done it with regular expressions — in fact the quantity-and-unit extractor gets 99.8% right with no AI at all. But normalising the rest, deciding that ground pepper, black peppers and ground black pepper are the same thing, is where an LLM earns its keep.

I set it up with LM Studio, locally, no cloud. And before pointing it at 1,388 recipes, I compared models on the same five.

For extraction, the small model is enough

Comparing qwen3-4b with llama-3.2-3b, the two get essentially the same answers. The differences are cosmetic:

Recipeqwen3-4bllama-3.2-3b
Trotters with chickpeasmanita, laurel, clavomanita de cerdo, hoja de laurel, clavo de olor
Rice with lobsterbrandybrandi
Battered cauliflowerhuevohuevos

Neither invents ingredients. Neither drops the important ones. The differences are singular versus plural and one spelling slip, and that is fixed by normalisation afterwards, not by a bigger model.

Pulling data out of text that already contains it is an easy task, and a 3B or 4B model on your laptop is plenty. Three hours of local inference, 18 batches of 100 recipes, without a single process failure.

For writing, it is not

When I moved to the next stage — writing a short blurb explaining each ingredient, in Migue’s voice — the same model fell apart:

“The lobster is a plant of the Rubiaceae family”

“Ñoras are broad-leaved perennial plants”

False and plausible at once, which is the worst possible combination. I tried Haiku and it got the hard facts right — lobster as a crustacean, ñora as a dried pepper — but the prose failed: mangled grammar, mismatched agreement, and it placed salt cod “in Castile”, hundreds of kilometres from any coast.

In a 60-word text signed by a Sevillian cook, those slips are visible from a mile away. The blurbs ended up written with Sonnet, and the prompt rules came straight out of those two failures: never attribute an ingredient to a region, enforce grammatical agreement, and leave a fact out rather than invent it.

The lesson is more useful than any model ranking: the task decides the model size, not the project. In one piece of work a local 4B ran extraction for three hours alongside a large model writing 50 short blurbs. Using the big one for everything would have burned money; using the small one for everything would have published that lobsters are plants.

The curve that tells you when you’re done

I did not expect this one and it is my favourite finding. Tracking how many new terms appeared in each batch of 100 recipes:

111 → 81 → 49 → 44 → 38 → 41 → 32 → 43 → 23 → 33
 → 21 → 26 → 24 → 18 → 26 → 18 → 26 → 1

From 111 new terms per 100 recipes down to one. Migue’s vocabulary is finite and it runs out. Twelve years of traditional Andalusian cooking fit into roughly 900 ingredients, and past recipe 1,200 almost nothing new shows up.

That curve is an objective stopping signal. You know you’re finished not because the recipes ran out, but because the vocabulary converged.

The distribution was telling too: 34 terms cover half the archive (salt, olive oil, garlic, onion…), and 437 terms appear in exactly one recipe. That long tail is not noise: burrata, suckling pig, gamoneu cheese, veal brains. An ingredient with a single recipe behind it is precisely what nobody can find today.

Salt is not a useful ingredient

When it was done, salt appeared in 1,073 recipes: 79% of the archive. As a navigation filter it distinguishes nothing — clicking “Salt” hands you back almost the entire site.

The term was deleted, with caveats: flor de sal, coarse salt and sea salt stay, because they say something about the dish, and the recipe text is untouched — salt is still in every ingredient list, because that is what Migue wrote. What disappears is the taxonomy term.

A good reminder that a correct piece of data can still be useless. The salt was extracted properly; it just wasn’t any good for navigating.

The failures that looked like the model’s

Three things I learned the slow way:

Lines go in separately, not flattened. Passing the recipe as running text lost ingredients deterministically. The same model, with the lines kept apart, didn’t fail.

Absorb the model’s typos, don’t fight them. It wrote ingrediento for ingrediente and champinón for champiñón. Rather than wrestling with the prompt, the code reads fields by prefix and merges by slug, keeping whichever spelling carries more accents. Cheaper and sturdier.

Almost everything that looked like a model failure came from the surrounding code: the parser, the counter, or a reference list I had written from memory and got wrong. When a result looks odd, read the raw response first. It was nearly always fine.

A side effect: the census exposed broken recipes

Extracting ingredients turned up lines that were not ingredients:

mantequilla 500 gr de fiambre surtido pollo
hr de patatas
mg de nata
gaseosa tambien nos servira agua con gas o sifon

That wasn’t extractor noise: these were recipes displaying incorrectly on the live site ever since the migration, which had merged into one line what the plain text didn’t separate cleanly. Between 40 and 80 cases.

I like this because the pattern recurs: systematically processing an old archive finds problems that have been sitting there for years. The taxonomy was the excuse; fixing those recipes is an improvement in its own right.

Where it stands

Recipes with Recipe schema1,358
Ingredients in the taxonomy749
Recipes linked1,360 (100%)
Average ingredients per recipe7.9
Active plugins2 (down from 5, including Live Composer and a cache for a host he no longer uses)

The site now has a browsable ingredient index, every recipe links its own, and the 91 categories were cleaned down to 89 with a coherent hierarchy and 301 redirects so the old URLs keep working.

Things remain. Around 36 recipes have malformed ingredient lists and need a manual pass. The ingredient blurbs are at 62%. And there are scattered errors in the taxonomy that Migue will correct — because what matters on that site is not the theme or the taxonomy, it is the recipes, and he cooked and wrote those over twelve years.

What I take away

The AI part was the easy one. Extracting ingredients with a local model worked first time and cost three hours of laptop. What decided the outcome was the dictionary: grouping variants, deciding that portobello mushrooms hangs off mushroom, eyeballing the long tail to separate real ingredients from junk. No model helps there; that is judgement.

And the judgement came from knowing the content. Knowing that salt is useless as a filter, that the blurbs Migue wrote by hand beat anything generated and had to be recovered, that thirteen featured images shaped the entire design. None of that comes out of a prompt: it comes from sitting down to look at what is there before you start building.

The code was fast. Everything else wasn’t.