skill
recipe-extract-skill
JSON-LD-first recipe extraction with LLM fallback, packaged as a Claude Code skill, with a per-path eval suite
Set up
Skills are installed by placing their folder where your agent loads skills.
The download comes from github.com/kimeyu/recipe-extract-skill itself, not from our server, so what you get is exactly what its author published.
Install instructions come from the README
README
synced Oct 23, 2026recipe-extract-skill
Extract a recipe from any URL -- title, ingredients, instructions, yield, times -- and present it cleanly, without ads, life stories, or SEO filler. Packaged as a Claude Code skill.
Part of a three-project portfolio on cost-aware AI engineering: recipe-extract-skill → game-retrieval-bench → retrieval-cost-bench
$ /extract-recipe https://www.allrecipes.com/recipe/16354/easy-meatloaf/
# Easy Meatloaf
**Yield:** 8 | **Total time:** 75 min | **Active time:** 15 min
## Ingredients
- 1.5 pounds ground beef
- 1 egg
- 1 onion (chopped)
- 1 cup milk
- 1 cup dried bread crumbs
- salt and pepper to taste
- 0.333 cup ketchup
- 2 tablespoons brown sugar
- 2 tablespoons prepared mustard
## Instructions
1. Gather the ingredients. Preheat the oven to 350 degrees F (175 degrees C). Lightly grease a 9x5-inch loaf pan.
2. Combine ground beef, onion, milk, bread crumbs, and egg in a large bowl, and season with salt and pepper. Press mixture evenly into the prepared loaf pan.
3. Stir ketchup, brown sugar, and mustard together in a small bowl until smooth, and spread evenly over the meatloaf.
4. Bake in the preheated oven until no longer pink in the center, about 1 hour. An instant-read thermometer inserted into the middle reads at least 160 degrees F (70 degrees C).
5. Serve hot and enjoy!
---
Source: https://www.allrecipes.com/recipe/16354/easy-meatloaf/
Why this is interesting
Many recipe sites embed schema.org/Recipe JSON-LD that any deterministic parser can extract in milliseconds. Reaching for an LLM on every page wastes tokens, latency, and reliability on a problem with a real answer in the source. Many other online recipes do not use the schema.org/Recipe JSON-LD. This skill parses the JSON-LD when it is there, and falls back to LLM extraction (Claude reading the cleaned HTML) only when structured data is missing or incomplete.
Three code paths emerge naturally:
jsonld-- the fast path. Sub-second, free, exact. Handles the bulk of mainstream recipe sites.hybrid-- JSON-LD present but missing a required field. LLM fills in only what is missing; the rest is deterministic.llm-- no usable structured data. Full extraction from HTML.
The eval suite reports per-path metrics so you can see exactly where the deterministic parser earns its keep and where the LLM is doing real work.
Architecture
URL --> fetcher --> jsonld parser
|
+--> Complete --------------------------------------> render --> user
|
+--> Partial --> Claude fills gaps --> validate --> render --> user
|
+--> NoStructured --> Claude extracts --> validate --> render --> user
recipe_extract/-- Python package: fetcher, JSON-LD parser, Pydantic schema, HTML cleaner, markdown renderer, CLI entry point.SKILL.md-- Claude Code skill that orchestrates: invokes the CLI, branches on the envelope status, handles the LLM-fill steps, validates, renders.evals/-- Hand-labeled corpus, runner, scorer, with on-disk caches for HTML and LLM responses so reruns are cheap.
Eval results
Latest run, N=31 URLs across three buckets (committed snapshot: evals/results.md).
Per-bucket:
| Bucket | n | title | ingr P | ingr R | qty | step delta | rougeL |
|---|---|---|---|---|---|---|---|
| A | 16 | 0.94 | 0.74 | 0.74 | 0.86 | 0 | 0.98 |
| B | 7 | 0.71 | 0.76 | 0.76 | 0.71 | 1 | 0.70 |
| C | 8 | 0.88 | 0.90 | 0.90 | 1.00 | 1.75 | 0.73 |
Per-path:
| Path | n | title | ingr P | ingr R | qty | step delta | rougeL |
|---|---|---|---|---|---|---|---|
| jsonld | 16 | 0.94 | 0.74 | 0.74 | 0.86 | 0 | 0.98 |
| hybrid | 7 | 0.71 | 0.76 | 0.76 | 0.71 | 1 | 0.70 |
| llm | 8 | 0.88 | 0.90 | 0.90 | 1.00 | 1.75 | 0.73 |
Totals: 0 errors, median latency 1.29s, 7 LLM calls (~80K input tokens, ~6K output tokens).
How to read this:
- Bucket A = sites with clean
schema.org/RecipeJSON-LD (AllRecipes, Simply Recipes, food blogs using a recipe plugin). - Bucket B = sites with partial JSON-LD, plus 6 synthetic fixtures built by stripping one field from real Bucket A pages. The hybrid path is exercised by having the LLM fill in only the missing piece.
- Bucket C = sites with no structured data at all (old Blogger blogs, Substack newsletters, brand sites that omit Recipe schema). LLM extracts the full recipe from the HTML body.
A few things visible in the numbers worth calling out:
- The deterministic path is excellent at title and instructions (0.94 / 0.98) and reasonable at ingredients (0.74). The ingredient gap reflects real-world divergence between how JSON-LD encodes ingredient strings versus how a human transcribes the source ("1.5 pounds ground beef" vs "1 1/2 pounds ground beef" both correctly represent the same value).
- The LLM-only path outperforms the hybrid path on title (0.88 vs 0.71) and ingredients (0.90 vs 0.76). That is a real (and counterintuitive) finding: when the LLM is asked to fill only one missing field, it tends to under-extract; when given full latitude to extract everything, it does better.
- Bucket B's step-count delta of 1.0 is mostly driven by the synthetic-instructions-stripped cases, where the LLM returns an empty list for the field it was told to fill rather than pulling from the HTML.
How it works
- JSON-LD path:
requests-> BeautifulSoup -> walk<script type="application/ld+json">blocks -> find a node with@type: Recipe(handles bare,@graph, andmainEntityshapes) -> map schema.org fields to a Pydantic model. Tolerant JSON parser handles trailing commas and HTML-entity-escaped string values seen in the wild. - Hybrid path: when the JSON-LD is missing a required field, the CLI emits an envelope flagging exactly what is missing and including a cleaned HTML excerpt. Claude reads the excerpt and emits JSON for only the missing field, which gets merged with the partial result and validated against the schema.
- LLM path: Claude extracts the full recipe from the HTML excerpt, validated against the same schema.
All three paths produce the same Pydantic-validated Recipe object. The skill renders it as markdown (for the user) and writes a JSON sidecar (for downstream tooling).
What this is not
Honest limitations and open questions, in roughly decreasing order of how much they matter:
- Not robust against TLS-fingerprinting bot detection. Some enterprise-CDN sites (Williams-Sonoma, Food Network confirmed) return HTTP 403 to Python's
requestsregardless of headers because they fingerprint the TLS handshake itself. A real fix would require something likecurl_cffiortls-clientto impersonate a browser's TLS signature. Deliberately out of scope; documented as a class of failure. - The hybrid path's narrow-fill prompt under-extracts. Three of seven Bucket B rows have catastrophic per-field failures: when asked to fill only the missing field (say,
instructions), the LLM returns an empty list rather than pulling from the HTML. The full-extraction LLM path (Bucket C) does not exhibit this. Tuning the narrow-fill prompt is the obvious fix; it is a known regression in the design and surfaces clearly in the eval. - Not a JS-rendered page handler. No Playwright. Sites that render their JSON-LD client-side return
js_renderedand the skill says so honestly rather than fabricating output. - Not a paywall bypass. Same story.
- Not nutrition / cuisine / ratings / author bio. Core fields only. Schema.org has them; they are inconsistently populated across sites and would dilute the eval signal without much added value.
- Not a rigorous benchmark. N=31, hand-picked, single-annotator. The eval is an honest illustration of methodology; what a rigorous version would require is straightforward to see from here.
A methodology lesson worth sharing
The eval corpus had to be re-bucketed mid-build when a real bug was found by running the eval itself. The fetcher initially advertised Accept-Encoding: gzip, deflate, br without having the brotli package installed; when sites returned brotli-encoded responses, requests handed back the raw compressed bytes, which the parser then saw as garbage. Six URLs that looked like Bucket C (no JSON-LD found) were actually Bucket A (clean JSON-LD that we could not decode). Adding brotli to dependencies and rerunning the eval revealed the misclassification. The corpus notes for those rows document the re-label. The broader point: an eval that surfaces bugs in the system being evaluated is doing its job.
Install
Requires Python 3.10+.
git clone https://github.com/kimeyu/recipe-extract-skill.git
cd recipe-extract-skill
python -m venv .venv
.venv/Scripts/pip install -e .[dev,eval] # POSIX: .venv/bin/pip install -e .[dev,eval]
(make install is also available if you have make.)
To register the skill with Claude Code, follow the skill install docs and point it at this directory. Or copy SKILL.md and recipe_extract/ into ~/.claude/skills/recipe-extract/.
For the eval suite (Bucket B and C URLs invoke Claude), set ANTHROPIC_API_KEY either as an environment variable or in a .env file at the repo root. The .env pattern is supported via python-dotenv and gitignored by default.
Usage
In Claude Code, share a recipe URL or invoke directly:
/extract-recipe https://www.allrecipes.com/recipe/16354/easy-meatloaf/
From the CLI directly (skips the LLM paths, prints the JSON envelope):
python -m recipe_extract https://www.allrecipes.com/recipe/16354/easy-meatloaf/
Development
.venv/Scripts/pytest -v # 100 tests
.venv/Scripts/ruff check . # lint
.venv/Scripts/python -m evals.run # full eval suite (requires ANTHROPIC_API_KEY)
(Or make test / make lint / make eval if you have make available.)
Design doc
docs/specs/2026-05-26-recipe-extract-skill-design.md -- full design rationale: why these particular code paths, eval bucket definitions, scoring metric choices, and what is intentionally left out.
License
MIT. See LICENSE.