Bonenkruid – Krachtige smaakmaker voor peulvruchten en vleesgerechten

Maak hier uw keuze
20 g (strooipotje)€5,95Op voorraad
35 g (scheppotje)€6,49Op voorraad
250 g (hersluitbare zak)€22,49Op voorraad
500 g (hersluitbare zak)€39,95Op voorraad
1 kg (hersluitbare zak)€67,95Op voorraad
% Volume voordeel
  • AantalPrijsKorting
  • €21,375%€20,2410%€19,1215%
Op voorraad Verwachte levertijd: 1 tot 4 werkdagen
Verplichte velden:
    Premium kwaliteit
    Alle onze prijzen zijn incl. BTW
    Ambachtelijk gemengdI Zonder additieven
    Co2 neutrale verzending
    Verzending €4,95 – Gratis vanaf €50
    100% natuurlijk, 100% gezond
    Informatie:Bonenkruid – Krachtige smaakmaker voor peulvruchten en vleesgerechten
    Ontdek de intense, licht pittige smaak van ons 100% natuurlijke bonenkruid. Ideaal bij bonen, linzen en vleesgerechten. Onmisbaar in jouw kruidenrek!

    Ontdek de veelzijdigheid van bonenkruid bij De Kruidenshop. Ons gedroogde bonenkruid is van de hoogste kwaliteit en perfect voor het verrijken van diverse gerechten. Van bonen en groenten tot soepen en stoofschotels, bonenkruid voegt een subtiele, kruidige smaak toe die je gerechten naar een hoger niveau tilt. Bestel vandaag nog bij De Kruidenshop en ontdek hoe bonenkruid je culinaire creaties kan verbeteren met zijn unieke aroma en smaak.

    Ingrediënten
    :
    100% natuurlijk: bonenkruid

    Allergenen:
    dit product bevat geen allergenen

    Voedingswaarden per 100g:
    Energie: 298 kcal
    Vet: 6 g
    Waarvan verzadigd: 0,1 g
    Koolhydraten: 54 g
    Waarvan suikers: 53 g
    Proteïnen: 7 g
    Zout: 0,2 g

    Ten minste houdbaar tot:
    zie verpakking (droog, donker en koel bewaren)

    Onze natuurlijke en unieke kruiden zijn gemaakt zonder toegevoegde aroma's, kleurstoffen, bewaarmiddelen of smaakversterkers! Onze producten worden verwerkt en bewaard in een bedrijf waar ook noten en pinda's verwerkt worden en kunnen ondanks alle voorzorgen sporen van allergenen bevatten.


    Heb je vragen?
    customers give us a 4,67/5 at Trusted-shops
    // Cloudflare Worker — dks-chat-rag v2.2 // - POST /ai : zoekt live in dekruidenshop.be (sitemap + page fetch), geeft antwoord met links // - Werkt zonder AI-key (deterministische samenvatting), met AI-key geeft natuurlijk antwoord // - Stuurt e-mailnotificatie bij elke vraag (MailChannels) export default { async fetch(req, env, ctx) { const url = new URL(req.url); if (req.method === "OPTIONS") return new Response(null, { headers: cors(req) }); if (url.pathname === "/") return json({ ok: true, info: "POST /ai" }, 200, cors(req)); if (url.pathname === "/ai" && req.method === "POST") { try { const body = await req.json().catch(() => ({})); const shop = body.shop || "De Kruidenshop"; const pageUrl = String(body.url || ""); const messages = Array.isArray(body.messages) ? body.messages : []; const userMsg = String(messages.find(m => m.role === "user")?.content || "").trim().slice(0, 2000); const q = normalize(userMsg || ""); const convoId = body.convoId || Math.random().toString(36).slice(2, 10); // 1) Zoek relevante documenten const docs = await retrieveDocs(env, q); // 2) Maak antwoord (AI als beschikbaar) const reply = await answerFromDocs(env, q, docs); // 3) Mail-notificatie (async, blokkeert de klant niet) ctx.waitUntil(sendEmail(env, { to: env.TO_EMAIL, from: env.FROM_EMAIL, subject: `Nieuwe chatvraag – ${shop} (#${convoId})`, text: emailText({ shop, convoId, pageUrl, userMsg, docs }), html: emailHtml({ shop, convoId, pageUrl, userMsg, docs }), }).catch(()=>{})); return json({ reply, convoId }, 200, cors(req)); } catch (e) { console.error("AI route error", e); return json({ reply: fallbackGeneric(), error: "server_error" }, 200, cors(req)); } } return json({ error: "not_found" }, 404, cors(req)); } }; /* ----------------- Config / Utils ----------------- */ const ORIGINS = ["https://www.dekruidenshop.be", "https://dekruidenshop.be"]; const TIMEOUT_MS = 8000; const MAX_SITEMAP_URLS = 800; const MAX_FETCH_PAGES = 24; const CACHE_HOURS = 6; const STOPWORDS = new Set(("de het een en of voor van met zonder op aan bij je jouw uw onze ons te tot naar is zijn was worden dan als maar ook om door dit dat deze die in uit over onder boven per het").split(" ")); function cors(req) { const origin = req.headers.get("origin") || ""; const allow = ORIGINS.includes(origin) ? origin : ORIGINS[0]; return { "Access-Control-Allow-Origin": allow, "Access-Control-Allow-Methods": "POST, OPTIONS", "Access-Control-Allow-Headers": "content-type", }; } function json(data, status=200, headers={}) { return new Response(JSON.stringify(data), { status, headers: { "content-type":"application/json", ...headers } }); } function esc(s){ return String(s||"").replace(/[&<>"]/g, m => ({ "&":"&", "<":"<", ">":">", '"':""" }[m])); } function stripTags(html){ return String(html||"") .replace(//gi," ") .replace(//gi," ") .replace(/<[^>]+>/g," ") .replace(/\s+/g," ") .trim(); } function normalize(s){ return s.toLowerCase() .normalize("NFD").replace(/[\u0300-\u036f]/g,"") .replace(/[^\p{Letter}\p{Number}\s\-&]/gu," ") .replace(/\s+/g," ") .trim(); } /* ----------------- Retrieval ----------------- */ function tokensOf(s){ const raw = normalize(s).split(" "); const out=[]; for(const t of raw){ if(t && !STOPWORDS.has(t)) out.push(t); } return out; } // Lichte fuzzy: enkelvoud/meervoud + 1-char afwijking function fuzzyHit(text, tok){ if (!text || !tok) return false; const t = text.toLowerCase(); if (t.includes(tok)) return true; if (tok.endsWith("en") && t.includes(tok.slice(0,-2))) return true; // kruiden ~ kruid if (tok.endsWith("s") && t.includes(tok.slice(0,-1))) return true; // thees ~ thee for(let i=0;i fetchDoc(u)))).filter(Boolean); // Scoren const scored = scoreAndSort(docs, toks, q).slice(0, 7); // Cache await env.CHAT_KV.put(key, JSON.stringify({ docs: scored }), { expirationTtl: CACHE_HOURS*3600 }); return scored; } function dedup(arr){ return Array.from(new Set(arr)); } function bestOrigin(){ return ORIGINS[0]; } async function discoverUrls(){ const base = bestOrigin(); let urls = []; const candidates = ["/sitemap_index.xml", "/sitemap.xml"]; for (const path of candidates){ try{ const r = await fetch(base+path, { cf: { cacheTtl: 900, cacheEverything: true } }); if(!r.ok) continue; const xml = await r.text(); let locs = Array.from(xml.matchAll(/([^<]+)<\/loc>/gi)).map(m=>m[1].trim()); // Als dit een index is met child sitemaps, haal die ook op const childSitemaps = locs.filter(u => /sitemap.*\.xml/i.test(u)); if (childSitemaps.length){ for(const sm of childSitemaps){ try{ const rr = await fetch(sm, { cf: { cacheTtl: 900, cacheEverything: true } }); if(rr.ok){ const x = await rr.text(); const ls = Array.from(x.matchAll(/([^<]+)<\/loc>/gi)).map(m=>m[1].trim()); locs = locs.concat(ls); } }catch(_){} } } urls = urls.concat(locs.filter(u => /^https:\/\/(www\.)?dekruidenshop\.be/i.test(u))); }catch(_){} if(urls.length) break; } if(!urls.length){ urls = [ base+"/", base+"/alle-producten-a-z/", base+"/kruiden/", base+"/kruidenmixen/", base+"/thee-matcha/", base+"/gerookte-knoflook/" ]; } // Normaliseren urls = Array.from(new Set(urls.map(u => u.replace(/\/+$/,"/")))).slice(0, MAX_SITEMAP_URLS); return urls; } function prefilter(urls, toks, rawQ){ if(!toks.length){ // generieke vraag → categorieën en A-Z return urls.filter(u => /(alle-producten-a-z|kruiden|kruidenmix|thee|matcha|product)/i.test(u)); } const res=[]; for(const u of urls){ const path = normalize(u.replace(/^https?:\/\/[^\/]+/,"")); let hit = 0; for(const t of toks){ if (path.includes(t) || fuzzyHit(path, t)) hit++; } if (hit>0 || /(alle-producten-a-z|kruiden|kruidenmix|thee|matcha|product)/i.test(path)) res.push(u); } // Query "thee" sterk boosten if (/\bthee\b/.test(rawQ)) { const picks = urls.filter(u => /thee|matcha/i.test(u)); res.unshift(...picks.slice(0, 30)); } return res; } async function fetchDoc(url){ try{ const ctrl = new AbortController(); const id = setTimeout(()=> ctrl.abort(), TIMEOUT_MS); const r = await fetch(url, { signal: ctrl.signal, cf: { cacheEverything:true, cacheTtl: 600 } }); clearTimeout(id); if(!r.ok) return null; const html = await r.text(); const title = (html.match(/]*>([\s\S]*?)<\/title>/i)?.[1] || "").trim(); const h1 = (html.match(/]*>([\s\S]*?)<\/h1>/i)?.[1] || "").trim(); const metaDesc = (html.match(/]+name=["']description["'][^>]+content=["']([^"']+)/i)?.[1] || "").trim(); // Probeer product-/categoriecontent let main = html.match(//i)?.[0] || html.match(/]+class=["'][^"']*(product__description|product-description|product__content|product__summary|content|description)[^"']*["'][\s\S]*?<\/div>/i)?.[0] || html; // Extra: pak product bullets/specs waar beschikbaar const specMatch = html.match(/]+class=["'][^"']*(product__specs|specs|features|product-spec|product__details)[^"']*["'][\s\S]*?<\/ul>/i); if (specMatch) main += "\n" + specMatch[0]; const text = stripTags(main).slice(0, 4000); const cleanTitle = stripTags(title || h1 || url); const desc = stripTags(metaDesc || ""); return { url, title: cleanTitle, desc, text }; }catch(_){ return null; } } function scoreAndSort(docs, toks, rawQ){ return docs.map(d => ({ ...d, score: scoreDoc(d, toks, rawQ) })) .sort((a,b)=> b.score - a.score); } function scoreDoc(doc, toks, rawQ){ const T = tokensOf((doc.title||"") + " " + (doc.desc||"")); const B = tokensOf(doc.text||""); let s = 0; if(!toks.length) s += 0.5; for(const t of toks){ if (T.includes(t) || fuzzyHit(T.join(" "), t)) s += 6; // body hits (cap bij 6) let cnt = 0; for(const w of B){ if (w===t) cnt++; else if (fuzzyHit(w, t)) cnt += 0.5; } s += Math.min(cnt, 6); if(doc.url.toLowerCase().includes(t)) s += 2; } // Query boosts if (/\bthee\b/.test(rawQ)) { if (/thee|matcha/i.test(doc.url) || /thee|matcha/i.test(doc.title)) s += 10; } // Productpagina’s licht boosten if (/\/product|\/p\//i.test(doc.url)) s += 3; return s; } /* -------------- Answering -------------- */ async function answerFromDocs(env, q, docs){ const showLinks = (list) => list.slice(0,5).map(d => `• ${d.title} → ${d.url}`).join("\n"); // AI pad if (env.LLM_API_URL && env.LLM_API_KEY && docs.length){ const sys = "Je bent een behulpzame support-assistent voor een Belgische kruidenwebshop. Antwoord feitelijk, kort en bruikbaar. Gebruik ALLEEN de meegegeven sitefragmenten; als info ontbreekt, geef dan links/suggesties i.p.v. te gokken."; const ctx = docs.slice(0,6).map((d,i)=>`[Bron ${i+1}] ${d.title}\nURL: ${d.url}\n${d.desc||""}\n---\n${(d.text||"").slice(0,800)}`).join("\n\n"); const prompt = `Vraag: "${q || "(onbekend)"}"\n\nBeschikbare sitefragmenten:\n${ctx}\n\nTaken:\n1) Geef een kort, direct antwoord (max 5 zinnen) en blijf bij de bronnen.\n2) Toon daarna 3–5 relevante links (titel → URL) uit de bronnen.\n3) Bij generieke zoektermen (zoals "thee", "matcha", "kruiden", "paprika"): geef een korte richting + categorie-link.\n4) Gebruik Nederlands (BE).`; const reply = await llm(env, [ { role:"system", content: sys }, { role:"user", content: prompt } ]); if (reply) return reply; } // Fallback (deterministisch) if (!docs.length) { return [ `Ik vond nog geen passende pagina’s op de webshop voor “${q || "je vraag"}”.`, `Kijk zeker in Thee & Matcha: https://www.dekruidenshop.be/thee-matcha/ of via Alle producten A–Z: https://www.dekruidenshop.be/alle-producten-a-z/` ].join(" "); } const links = showLinks(docs); let extra = ""; if (q.includes("thee") || q.includes("matcha")) { extra = `\n\nCategorie Thee & Matcha → https://www.dekruidenshop.be/thee-matcha/`; } return `Dit lijkt relevant voor “${q || "je vraag"}”:\n${links}${extra}\n\n(Automatisch gevonden op dekruidenshop.be.)`; } async function llm(env, messages){ const r = await fetch(`${env.LLM_API_URL.replace(/\/+$/,"")}/v1/chat/completions`, { method:"POST", headers:{ "content-type":"application/json", "authorization":`Bearer ${env.LLM_API_KEY}` }, body: JSON.stringify({ model: env.LLM_MODEL || "gpt-4o-mini", temperature: 0.2, messages }) }); if(!r.ok) return ""; const data = await r.json(); return data?.choices?.[0]?.message?.content || ""; } function fallbackGeneric(){ return "Ik help je graag! Kun je je vraag iets specifieker maken (product, smaak, toepassing)? Je kunt ook zoeken via ‘Alle producten A–Z’ of onze categorieën (Kruiden, Kruidenmixen, Thee & Matcha)."; } /* -------------- E-mail -------------- */ async function sendEmail(env, { to, from, subject, text, html }) { if (!to || !from) return; const payload = { personalizations: [{ to: [{ email: to }] }], from: { email: from, name: "De Kruidenshop – Chatmelding" }, subject: subject || "Nieuwe chatvraag", content: [ { type: "text/plain", value: text || "" }, { type: "text/html", value: html || "" }, ], }; await fetch("https://api.mailchannels.net/tx/v1/send", { method: "POST", headers: { "content-type":"application/json" }, body: JSON.stringify(payload) }); } function emailText({ shop, convoId, pageUrl, userMsg, docs }){ const lines = [ `Nieuwe chatvraag op ${shop}`, `Conversatie ID: #${convoId}`, pageUrl ? `Pagina: ${pageUrl}` : "", "", "Vraag:", userMsg || "(leeg)", "", "Topresultaten:", ...(docs||[]).slice(0,5).map(d => `- ${d.title} -> ${d.url}`), "", `Tijd: ${new Date().toISOString()}` ].filter(Boolean); return lines.join("\n"); } function emailHtml({ shop, convoId, pageUrl, userMsg, docs }){ const items = (docs||[]).slice(0,5).map(d => `
  • ${esc(d.title)}
  • `).join(""); return `

    Nieuwe chatvraag op ${esc(shop)}

    Conversatie ID: #${esc(convoId)}

    ${pageUrl ? `

    Pagina: ${esc(pageUrl)}

    ` : ""}
    Vraag
    ${esc(userMsg || "(leeg)")}
    Topresultaten
      ${items || "
    1. (geen)
    2. "}

    Verzonden: ${new Date().toLocaleString("nl-BE")}

    `; } /* -------------- Misc -------------- */ function hash16(s){ let h=0, i=0, l=s.length; while (i>>0).toString(16); }