Purpose
Search Chefkoch.de's catalog (≈384,000 recipes) for those matching one or more diet/allergen filters and optionally a keyword, returning each recipe's title, canonical URL, image, rating + vote count, preparation time, difficulty, short description, and recipe ID. Also covers the daily "Was koche ich heute?" suggestion stream when no diet filter is requested. Read-only — never posts, rates, saves to Kochbuch, or interacts with the Wochenplaner.
When to Use
- "Find me vegan main-course recipes rated ≥4 stars."
- "Give me 30-minute low-carb dinners."
- "What's on Chefkoch's daily 'Was koche ich heute' picks?"
- "Glutenfreie Suppe für 4 Personen" (free-text keyword + diet tag).
- Bulk extraction of search-result metadata for meal-planning agents, recipe aggregators, or RAG pipelines targeting German-language cooking content.
Workflow
Chefkoch's search-result page is fully server-rendered into a stable URL grammar with embedded data-vars-* attributes on every recipe card and a parallel <script type="application/ld+json"> ItemList. There is no JSON API, no GraphQL, and no XHR endpoint needed — a single browserless_agent goto of the URL renders the full result set (status 200, no anti-bot stealth required), and you extract the cards in-page. Lead with the URL-param path; reserve interactive UI driving only for inspecting the filter modals, which the agent shouldn't need to do at runtime.
1. Construct the search URL
URL grammar (all modifiers optional except s<page>):
https://www.chefkoch.de/rs/s<page>[t<id>,<id>,…][r<minRating>][p<maxMinutes>][o<sort>][/<query>]/<slug>-Rezepte.htmls<N>— page index, zero-based.s0= first page (≤30 cards),s1= next page, etc. The trailing-Rezepte.htmlslug is informational only — the server canonicalizes whatever you submit, so you can pass/Rezepte.htmland follow the 301 to learn the canonical slug. A request withs<N>beyond the available page count 301-redirects back tos1(verifieds30→s1for vegan). ReadLocationheader to detect exhaustion.t<id>[,<id>,…]— comma-separated tag IDs from the table below. The server auto-sorts ascending and rewrites the URL via 301 (e.g.t57,21→t21,57). Combining tags ANDs them:t21,57returns recipes that are bothHauptspeiseANDVegan.r<n>— minimum average rating, 1..5. Decimal allowed (r4.5).r4returned 18,491 vegan recipes vs. 26,393 unfiltered.p<minutes>— maximum preparation time ("Arbeitszeit") in literal minutes. Verified values:p15,p30,p60,p120. The UI slider in the modal exposes 5 stops (15/30/60/120/unlimited) but the URL accepts any positive integer.o<n>— sort order:o2Empfehlung (default — can be omitted),o3Bewertung (best-rated first),o6Neuheiten (newest first). Other values 301 back to default./<query>/— optional free-text keyword between the modifier block and the slug, e.g./rs/s0t57/glutenfrei/Rezepte.htmlfor vegan + "glutenfrei" keyword. Use this for allergens/ingredients not in the diet enum (gluten-free, laktosefrei, nussfrei, sojafrei, etc.) — Chefkoch's diet enum doesn't cover specific allergens beyond Vegan/Vegetarisch, so allergy intent falls back to keyword.
Tag ID reference (complete enum, extracted 2026-05-20 from /rs/s0/Rezepte.html)
Diet (Ernährung) — primary group for this skill:
| id | label | id | label |
|---|---|---|---|
| 32 | Vegetarisch | 7710 | Paleo |
| 57 | Vegan | 56 | Fettarm |
| 55 | Kalorienarm | 112 | Trennkost |
| 9948 | Low Carb | 143 | Vollwert |
| 9947 | Ketogen | 111 | Für das Baby |
Meal type (Mahlzeit): 21 Hauptspeise · 19 Vorspeise · 36 Beilage · 90 Dessert · 71 Snack · 53 Frühstück
Category (Rezeptkategorie): 30 Auflauf · 82 Pizza · 94 Reis-/Nudelsalat · 15 Salat · 3669 Salatdressing · 122 Tarte · 52 Fingerfood · 35 Dips · 34 Saucen · 40 Suppe · 166 Klöße · 108 Brot/Brötchen · 46 Brotspeise · 51 Aufstrich · 89 Süßspeise · 127 Eis · 92 Kuchen · 147 Kekse · 93 Torte · 157 Confiserie · 11 Getränke · 113 Shake · 313 Gewürzmischung · 243 Pasten · 211 Studentenküche
Characteristic (Rezepteigenschaften): 50 Einfach · 49 Schnell · 79 Basisrezepte · 48 Preiswert
Cuisine (Länderküche): 65 Deutsch · 28 Italienisch · 43 Spanisch · 149 Portugiesisch · 84 Französisch · 117 Englisch · 86 Osteuropäisch · 133 Skandinavisch · 44 Griechisch · 103 Türkisch · 212 Russisch · 163 Naher Osten · 14 Asiatisch · 13 Indisch · 148 Japanisch · 38 Amerikanisch · 74 Mexikanisch · 95 Karibisch · 114 Lateinamerikanisch · 101 Afrikanisch · 131 Marokkanisch · 168 Ägyptisch · 145 Australisch
Occasion (Anlass): 78 Frühling · 27 Sommer · 99 Herbst · 64 Winter · 91 Für Kinder · 120 Ostern · 39 Halloween · 102 Weihnachten · 87 Silvester · 106 Festlich · 63 Grillen · 152 Camping · 45 Party
Technique (Zubereitung): 144 Kochen · 69 Braten · 107 Dünsten · 153 Blanchieren · 66 Schmoren · 23 Backen · 137 Überbacken · 116 Wok · 135 Mikrowelle · 162 Römertopf · 62 Fondue · 136 Marinieren · 134 Frittieren · 128 Flambieren · 150 Haltbarmachen · 1147 Wursten
Example URLs (all return HTTP 200, fully rendered HTML with 30 recipe cards on s0):
# Vegan, sorted by rating, min 4 stars
https://www.chefkoch.de/rs/s0t57r4o3/Vegan-Rezepte.html # 18,491 results
# Low-carb main courses, min 4 stars, sorted by rating
https://www.chefkoch.de/rs/s0t21,9948r4o3/Hauptspeise-Low-Carb-Rezepte.html
# Vegetarian + glutenfrei keyword, max 30 minutes
https://www.chefkoch.de/rs/s0t32p30/glutenfrei/Vegetarisch-Rezepte.html
# Vegan pasta dishes
https://www.chefkoch.de/rs/s0t57/pasta/Vegan-Rezepte.html # 2,767 results2. Render the page and capture the HTML
[
{
"method": "goto",
"params": { "url": "<URL>", "waitUntil": "load", "timeout": 45000 }
},
{ "method": "html", "params": { "selector": "body" } }
]Run both as commands in one browserless_agent call. A residential proxy (proxy: { proxy: "residential", proxyCountry: "us" } as a top-level arg) is optional — Chefkoch has no anti-bot enforcement against direct US IPs in our testing — but is cheap insurance against future enforcement; no extra stealth flag is needed. The html command returns the rendered body markup; run the step-3 regexes over it. For a leaner result, fold the parsing into an evaluate that walks the cards in-page and returns the projected rows directly (staying under the ~200k-char text-return cap) instead of shipping raw HTML back.
3. Parse the total result count + recipe cards
import re, json, html
# Total count (German number format, '.' as thousands separator)
m = re.search(r'(\d[\d.]+)\s*Rezepte', content)
total = int(m.group(1).replace('.', '')) if m else None
# Each recipe card has the same shape — extract by data-vars-* attributes
card_re = re.compile(
r'data-vars-position="(?P<pos>\d+)"\s+'
r'data-vars-tracking-id="recipe-(?P<id>\d+)"\s+'
r'data-vars-recipe-title="(?P<title>[^"]+)"'
r'.{0,200}?data-vars-num-votes="(?P<votes>\d+)"\s+'
r'data-vars-rating="(?P<rating>[\d.]+)"\s+'
r'data-vars-has-video="(?P<video>[01])"',
re.DOTALL,
)
# Within each card, time + difficulty live in <div class="ds-recipe-info">
# with two <span class="ds-recipe-info__text"> children:
# 1st: "10 Min." | "1 Std." | "2 Std. 15 Min."
# 2nd: "simpel" | "normal" | "pfiffig"
info_re = re.compile(
r'<div class="ds-recipe-info">.+?'
r'<span class="ds-recipe-info__text"[^>]*>([^<]+)</span>.+?'
r'<span class="ds-recipe-info__text"[^>]*>([^<]+)</span>',
re.DOTALL,
)
# Canonical URL is the first <a class="ds-recipe-card__link"> href
url_re = re.compile(r'<a class="ds-recipe-card__link[^"]*"\s+href="([^"]+)"')
# Description (1-line teaser, optional)
desc_re = re.compile(r'<div class="ds-recipe-card__description[^"]*">([^<]+)</div>')
# Image (full-size 640x800 lives in srcset)
img_re = re.compile(r'<img alt="[^"]+"\s+loading="lazy"\s+src="([^"]+)"')The JSON-LD ItemList block (<script type="application/ld+json">) is a parallel, lighter-weight surface that gives you {position, url, name, description} per recipe without HTML scraping — use it when you only need title + URL and want to skip data-vars regex. Cross-check: both surfaces return 42 items per s0 page on filtered queries, 30 on others. (The page-DOM data-vars-position count and the JSON-LD count match exactly per response.)
4. Paginate
Increment s<N> until the page redirects you back to s1 (signaling exhaustion) or you hit your target N. A browser goto auto-follows the 301, so detect exhaustion by reading the landed URL in-page: after each goto, append { "method": "evaluate", "params": { "content": "location.pathname" } } (value comes back under .value) — if you requested s<N> with N≠1 and it lands on /rs/s1, you're past the last page.
# each iteration = one browserless_agent call: goto s{page} → html/evaluate → read location.pathname
page = 0
while True:
url = f"https://www.chefkoch.de/rs/s{page}t57r4o3/Vegan-Rezepte.html"
r = render_page(url) # goto + capture HTML + landed pathname
if page != 1 and r['landed_pathname'].startswith('/rs/s1'):
break # redirected back to s1 → exhausted
cards = parse(r['html'])
if not cards: break
yield from cards
page += 1Page size is ~30 cards (sometimes 42 with embedded "you-might-also-like" tail). Plan request budget around ceil(total / 30) pages.
5. Daily "Was koche ich heute?" (no filter)
When the user has no diet/keyword intent and just wants today's editorial picks:
[
{
"method": "goto",
"params": {
"url": "https://www.chefkoch.de/rezepte/was-koche-ich-heute/",
"waitUntil": "load",
"timeout": 45000
}
},
{ "method": "html", "params": { "selector": "body" } }
]Returns 8 hand-curated recipes for the day. Extract them from the page's recipe URLs (https://www.chefkoch.de/rezepte/<id>/<slug>.html) — the JSON-LD on this page is a CollectionPage, not an ItemList, so recipe URLs surface via plain anchor regex rather than schema.org structure. The same card shape (data-vars-rating, data-vars-num-votes, etc.) is not present on this page; titles come from the anchor's title= attribute or surrounding <h3>.
Browser fallback
Only needed if you want to (a) trigger fresh-Vue rendering of a non-public filter the URL grammar doesn't expose, or (b) demo the diet-modal UI to a human. The runtime skill should not need this. If you do drive the UI, batch the whole flow in one browserless_agent commands array — it saves round-trips and avoids accidentally dropping the session config:
// browserless_agent commands array
[
{
"method": "goto",
"params": {
"url": "https://www.chefkoch.de/rs/s0/Rezepte.html",
"waitUntil": "load",
"timeout": 45000,
},
},
// Dismiss the cookie consent modal — required before any interaction
{ "method": "snapshot" }, // find button "Allen Zwecken zustimmen"
{
"method": "click",
"params": { "selector": "button:has-text('Allen Zwecken zustimmen')" },
},
// Click filter chip "Ernährung", check diet boxes, click "Anwenden" (confirm selectors via snapshot if they miss)
{ "method": "click", "params": { "selector": "<Ernährung chip>" } },
{ "method": "click", "params": { "selector": "<Anwenden>" } },
// Read the URL the page navigated to — it is exactly the URL grammar above
{ "method": "evaluate", "params": { "content": "location.href" } },
]No session-release step — there's nothing to release. The session persists across calls, keyed by proxy/profile (repeat the same config on each call to reconnect; dropping or changing it lands you in a different, blank session), so batching the dismiss → filter → read-URL flow inside one call is just a convenience that saves round-trips. The cookie consent dialog (Willkommen bei Chefkoch — Allen Zwecken zustimmen) blocks interactive clicks until dismissed, but the recommended goto + in-page extract reads the server-rendered cards and JSON-LD out of the DOM regardless of the overlay, so it is irrelevant for the URL-param path.
Site-Specific Gotchas
- No anti-bot enforcement on direct HTTP fetches in 2026-05-20 testing — bare
curl+User-Agentwould likely work, and a plainbrowserless_agentgoto(optionally with a residential proxy) certainly works (status 200, no captcha, no Akamai). Don't waste time on stealth sessions for this skill. - Server canonicalizes tag order and rewrites URL: submitting
t57,21→ 301 tot21,57/Hauptspeise-Vegan-Rezepte.html. A browsergotoauto-follows the 301 chain, so you land on the canonical URL Chefkoch indexes — readlocation.hrefin-page if you need to record it. - Slug at end is informational only —
/Vegan-Rezepte.html,/Hauptspeise-Vegan-Rezepte.html, even/Rezepte.htmlall work for the same tag set. The server uses the modifier block to compute its own slug and 301s if you submit a "wrong" one. Don't try to reconstruct the slug from tag names; just submit/Rezepte.htmland let the server rewrite. - Pagination beyond the result set 301s to
s1rather than serving an empty page. Use this as a clean exhaustion signal: if you sents<N>and the response is301 Location: /rs/s1..., you've gone past the last page. - No dedicated allergen filter beyond Vegan/Vegetarisch: the diet enum (10 values) covers vegetarian, vegan, low-carb, ketogen, paleo, low-fat, low-cal, Trennkost (food-combining), Vollwert (whole-foods), and Baby — but does not include glutenfrei, laktosefrei, nussfrei, sojafrei, fructosearm, or histaminarm. For those, fall back to free-text keyword search via the
/<query>/segment (e.g./rs/s0t32p30/glutenfrei/Rezepte.html). The keyword matches recipe titles, descriptions, ingredient lists, and tags. r<n>rating is a minimum threshold, not a tier —r4means ≥4.00, not "4-star tier". Decimal precision works (r4.5→ 10,505 vegan results vs.r4→ 18,491).o2sort is the default and can be omitted — submittingo2explicitly will 301-redirect to a URL without theomodifier. Onlyo3(rating) ando6(newest) survive in canonical URLs.p<minutes>is in literal minutes, not the slider's 1–5 index. The UI slider snaps to 15/30/60/120; the URL accepts any positive integer.- German thousands separator in result count: "26.393 Rezepte" means 26,393, not 26.393. Strip the period before parsing.
- Card count per page varies: filtered queries return ~42 cards/page (30 results + 12 cross-sell at tail); broad queries return 30. The
data-vars-positionattribute is the authoritative item index — trust it overlen(matches). - JSON-LD
ItemListis lossy — it carries{position, url, name, description}but no rating, vote count, prep time, difficulty, or image. Use the data-vars regex on the card HTML for full metadata, or hit each recipe page individually for richer schema.orgRecipedata. - Difficulty enum is fixed:
simpel,normal,pfiffig. Not localized in the URL. - Cooking-time strings are not zero-padded —
"5 Min.","45 Min.","1 Std.","1 Std. 30 Min.". Parse as duration; don't assume two-digit minutes. - "Was koche ich heute?" page does NOT carry
data-vars-*on cards and uses aCollectionPageJSON-LD type, notItemList— its 8 daily recipes need plain-anchor extraction. Use a different parser than the search-result parser for this page. - Cookie consent (
dialog: Willkommen bei Chefkoch) blocks interactive clicks — only relevant if you drive the filter UI. The "Allen Zwecken zustimmen" button (confirm the ref viasnapshot) is what dismisses it. Agoto+ in-page extract reads the server-rendered cards regardless of the overlay, so it is unaffected. - PLUS-paywalled recipes exist as a small fraction of results, marked by
data-vars-plus-content="1"(and the "Nur PLUS-Rezepte" filter toggle). They surface in the result list but the recipe detail page is gated behind a subscription. The skill should still return them as listed; downstream consumers can filter on theis_plusflag.
Expected Output
For a typical filtered query (/rs/s0t57r4o3/Vegan-Rezepte.html):
{
"query": {
"url": "https://www.chefkoch.de/rs/s0t57r4o3/Vegan-Rezepte.html",
"tags": [{ "id": 57, "label": "Vegan", "group": "diet" }],
"min_rating": 4.0,
"max_prep_minutes": null,
"sort": "rating",
"keyword": null,
"page": 0
},
"total_results": 18491,
"page_size": 30,
"recipes": [
{
"position": 1,
"id": "4410071768223128",
"title": "Veganes Gnocchi-Ragout",
"url": "https://www.chefkoch.de/rezepte/4410071768223128/Veganes-Gnocchi-Ragout.html",
"image": "https://img.chefkoch-cdn.de/rezepte/4410071768223128/bilder/1614973/crop-640x800/veganes-gnocchi-ragout.jpg",
"rating": 4.71,
"num_votes": 7,
"prep_time_minutes": 10,
"prep_time_raw": "10 Min.",
"difficulty": "normal",
"description": "vegan",
"has_video": true,
"is_plus": false
}
]
}For the daily-suggestions page (/rezepte/was-koche-ich-heute/):
{
"query": {
"url": "https://www.chefkoch.de/rezepte/was-koche-ich-heute/",
"type": "daily_suggestions"
},
"date": "2026-05-20",
"total_results": 8,
"recipes": [
{
"position": 1,
"id": "111751046790147",
"title": "Käsesuppe",
"url": "https://www.chefkoch.de/rezepte/111751046790147/Kaesesuppe.html",
"rating": null,
"num_votes": null,
"prep_time_minutes": null,
"difficulty": null
}
]
}For an exhausted pagination request (s<N> past last page):
{
"query": {
"url": "https://www.chefkoch.de/rs/s99t57/Vegan-Rezepte.html",
"page": 99
},
"total_results": 18491,
"page_size": 0,
"recipes": [],
"exhausted": true,
"redirected_to": "https://www.chefkoch.de/rs/s1t57/Vegan-Rezepte.html"
}For an unknown allergen the diet enum doesn't cover (glutenfrei, laktosefrei, nussfrei), combine a related diet tag with the keyword segment:
{
"query": {
"url": "https://www.chefkoch.de/rs/s0t32p30/glutenfrei/Rezepte.html",
"tags": [{ "id": 32, "label": "Vegetarisch", "group": "diet" }],
"keyword": "glutenfrei",
"max_prep_minutes": 30,
"note": "Chefkoch's diet enum has no glutenfrei tag — falling back to keyword match."
}
}