Purpose
Extract structured recipe information from the originating Okusno.je article and return its main image URL, which may be hosted on images.24ur.com.
When to Use
Use when the caller provides an Okusno.je recipe URL or otherwise identifies the originating recipe article and needs the title, ingredients, preparation procedure, and principal recipe photograph. The images.24ur.com URL alone is an image asset and does not contain the recipe metadata; do not attempt to infer the article from its opaque filename.
Workflow
- Navigate directly to
{recipe-url}onokusno.jewithwaitUntil: domcontentloaded. - In one page evaluation, dismiss
button#didomi-notice-agree-buttonif present, then run this extractor:
(() => {
const parse = (s) => {
try {
return JSON.parse(s);
} catch {
return null;
}
};
const walk = (value) => {
if (!value) return [];
if (Array.isArray(value)) return value.flatMap(walk);
if (typeof value === "object")
return [value, ...Object.values(value).flatMap(walk)];
return [];
};
const jsonLd = [...document.querySelectorAll('script[type="application/ld+json"]')]
.map((s) => parse(s.textContent))
.filter(Boolean);
const recipe =
walk(jsonLd).find((x) => {
const t = x && x["@type"];
return t === "Recipe" || (Array.isArray(t) && t.includes("Recipe"));
}) || {};
const text = (v) => (typeof v === "string" ? v.trim() : "");
const ingredients = Array.isArray(recipe.recipeIngredient)
? recipe.recipeIngredient.map(text).filter(Boolean)
: [
...document.querySelectorAll(
'[itemprop="recipeIngredient"], .recipe-ingredients li, .ingredients li',
),
]
.map((e) => e.textContent.trim())
.filter(Boolean);
const instructionText = (v) => {
if (typeof v === "string") return v.trim();
if (Array.isArray(v)) return v.map(instructionText).filter(Boolean).join("\n");
if (v && typeof v === "object") return text(v.text || v.name);
return "";
};
const procedure = Array.isArray(recipe.recipeInstructions)
? recipe.recipeInstructions.map(instructionText).filter(Boolean)
: [instructionText(recipe.recipeInstructions)].filter(Boolean);
const imageValue = recipe.image;
const imageCandidates = Array.isArray(imageValue) ? imageValue : [imageValue];
let mainImage =
imageCandidates
.map((x) => (typeof x === "string" ? x : x && (x.url || x.contentUrl)))
.find(Boolean) || "";
if (!mainImage) {
const candidates = [...document.querySelectorAll("article img, main img, img")]
.map((i) => ({
url: i.currentSrc || i.src,
area: (i.naturalWidth || 0) * (i.naturalHeight || 0),
}))
.filter((x) => x.url && x.area > 150000)
.sort((a, b) => b.area - a.area);
mainImage = candidates[0]?.url || "";
}
return {
title:
text(recipe.name) ||
document.querySelector("h1")?.textContent.trim() ||
document.title,
ingredients,
procedure,
mainImage,
};
})();Site-Specific Gotchas
- The recipe content is on
okusno.je;images.24ur.comis the associated CDN and serves the photograph only. - Recipe metadata is normally exposed in
application/ld+jsonas aRecipeobject, potentially nested inside an@graph; search recursively rather than assuming the first JSON-LD script is the recipe. - The consent banner uses
button#didomi-notice-agree-button; dismiss it only when present. The extractor otherwise reads the current page and makes no external requests. - Prefer
recipe.imagefrom JSON-LD because ordinary page image lists can include logos, thumbnails, advertisements, and related recipes. Use the large-image fallback only when structured image metadata is absent. - Never fabricate an article URL from an opaque CDN filename. If only the final image URL is available, return the image URL but report that the originating recipe page must be supplied separately.
Expected Output
Return an object with exactly these fields:
title: recipe titleingredients: ordered array of ingredient stringsprocedure: ordered array of preparation-step stringsmainImage: primary image URL, commonly animages.24ur.comURL