700 lines
29 KiB
Python
700 lines
29 KiB
Python
|
|
#!/usr/bin/env python3
|
|||
|
|
"""Generate the Cucina Food static merksite from harvested live content."""
|
|||
|
|
from __future__ import annotations
|
|||
|
|
|
|||
|
|
import html as htmlmod
|
|||
|
|
import json
|
|||
|
|
import re
|
|||
|
|
import shutil
|
|||
|
|
import urllib.request
|
|||
|
|
from pathlib import Path
|
|||
|
|
|
|||
|
|
ROOT = Path(__file__).resolve().parent
|
|||
|
|
CDN = "https://www-static.cucinafood.nl/wp-content/uploads"
|
|||
|
|
LOGO = f"{CDN}/2025/02/Group-1000002463-1.png"
|
|||
|
|
UA = {"User-Agent": "Mozilla/5.0 Cucina redesign"}
|
|||
|
|
|
|||
|
|
LINE_META = {
|
|||
|
|
"to-cook": {
|
|||
|
|
"name": "To Cook",
|
|||
|
|
"kicker": "Halal maaltijdpakketten",
|
|||
|
|
"hero": f"{CDN}/2025/02/Pasta-Pesta-Red-chili-kip.webp",
|
|||
|
|
"intro": "Kook de kleurrijke wereldkeuken met alles-in-1 halal maaltijdpakketten. Verse groenten, een receptkaart, binnen ongeveer 30 minuten op tafel.",
|
|||
|
|
"blurb": "Alles-in-1 · ±30 min · receptkaart in de doos",
|
|||
|
|
"points": [
|
|||
|
|
"Elke maaltijd bevat verse groenten",
|
|||
|
|
"Alles in één pakket, geen extra boodschap",
|
|||
|
|
"Duurzaam verpakt en recyclebaar",
|
|||
|
|
"Binnen 30 minuten op tafel",
|
|||
|
|
],
|
|||
|
|
},
|
|||
|
|
"to-heat": {
|
|||
|
|
"name": "To Heat",
|
|||
|
|
"kicker": "Ovenschotels & kant-en-klare maaltijden",
|
|||
|
|
"hero": f"{CDN}/2025/10/cucinaa-foods.webp",
|
|||
|
|
"intro": "Geen voorbereiding: opwarmen en eten. Ovenschotels en maaltijden uit de halal wereldkeuken, in ovenbestendige schalen.",
|
|||
|
|
"blurb": "Opwarmen · klaar om te eten",
|
|||
|
|
"points": [
|
|||
|
|
"Smaakvolle gerechten uit de halal wereldkeuken",
|
|||
|
|
"De marinades en kruiden zitten erin",
|
|||
|
|
"Ovenbestendige ready-to-eat schalen",
|
|||
|
|
"Verwarm. Geniet. Klaar.",
|
|||
|
|
],
|
|||
|
|
},
|
|||
|
|
"to-go": {
|
|||
|
|
"name": "To Go",
|
|||
|
|
"kicker": "Halal maaltijdsalades",
|
|||
|
|
"hero": f"{CDN}/2025/02/Cutting-Borad-and-Tomato.webp",
|
|||
|
|
"intro": "Smaakvolle halal maaltijdsalades voor onderweg of als lunch. Verrassende combinaties, in een TO GO-verpakking.",
|
|||
|
|
"blurb": "Lunch · onderweg · kant-en-klaar",
|
|||
|
|
"points": [
|
|||
|
|
"Verpakt in handige TO GO-verpakkingen",
|
|||
|
|
"Snel en vullend, zonder gedoe",
|
|||
|
|
"Wereldse smaakcombinaties in één bak",
|
|||
|
|
],
|
|||
|
|
},
|
|||
|
|
"halal-meat": {
|
|||
|
|
"name": "Halal Meat",
|
|||
|
|
"kicker": "Premium kwaliteit halal vlees",
|
|||
|
|
"hero": f"{CDN}/2025/10/halal_meat_chicken.webp",
|
|||
|
|
"intro": "Halal rund, lam, kalf en kip van Hollandse kwaliteit. Gewoon in het schap van je supermarkt, gecertificeerd volgens strikte halalnormen.",
|
|||
|
|
"blurb": "Halal gecertificeerd · in de supermarkt",
|
|||
|
|
"points": [
|
|||
|
|
"Premium kwaliteit halal vlees",
|
|||
|
|
"Verkrijgbaar bij je lokale supermarkt",
|
|||
|
|
"Halal gecertificeerd volgens strikte normen",
|
|||
|
|
],
|
|||
|
|
},
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
RECIPE_FOR = {
|
|||
|
|
"rode pasta pesto": "rode-pasta-pesto-met-gehaktballetjes",
|
|||
|
|
"sticky tempeh": "sticky-tempeh-met-rijst-en-gewokte-groenten",
|
|||
|
|
"enchilada’s": "enchilada-met-bonen-en-mexicaanse-yoghurtdip",
|
|||
|
|
"enchiladas": "enchilada-met-bonen-en-mexicaanse-yoghurtdip",
|
|||
|
|
"gele kokoscurry": "gele-kokoscurry-met-kip",
|
|||
|
|
"pad thai": "pad-thai-met-limoenwortel-en-pindas",
|
|||
|
|
"gevulde puntpaprika": "gevulde-puntpaprika-met-bulgur-gehakt-en-echte-feta",
|
|||
|
|
"penne": "penne-met-kipburgertjes-en-citroen-ricotta",
|
|||
|
|
"köfte": "kipkofte-met-couscous-en-4-soorten-groenten",
|
|||
|
|
"kofte": "kipkofte-met-couscous-en-4-soorten-groenten",
|
|||
|
|
"kip teriyaki": "kip-teriyaki-met-gewokte-broccoli",
|
|||
|
|
"kruidige arabische hutspot": "kruidige-arabische-hutspot-met-gebakken-ui-merguez-worstjes",
|
|||
|
|
"risotto": "risotto-met-gemarineerde-kip-en-kastanjechampignons",
|
|||
|
|
"gevulde zoete aardappel": "gevulde-zoete-aardappel-met-geroosterde-kikkererwten-oosterse",
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
|
|||
|
|
def get(url: str) -> str:
|
|||
|
|
req = urllib.request.Request(url, headers=UA)
|
|||
|
|
with urllib.request.urlopen(req, timeout=30) as r:
|
|||
|
|
return r.read().decode("utf-8", "replace")
|
|||
|
|
|
|||
|
|
|
|||
|
|
def strip_tags(s: str) -> str:
|
|||
|
|
return re.sub(r"<[^>]+>", "", htmlmod.unescape(s)).strip()
|
|||
|
|
|
|||
|
|
|
|||
|
|
def textify(html: str) -> str:
|
|||
|
|
clean = re.sub(r"<script[\s\S]*?</script>", " ", html, flags=re.I)
|
|||
|
|
clean = re.sub(r"<style[\s\S]*?</style>", " ", clean, flags=re.I)
|
|||
|
|
clean = re.sub(r"<br\s*/?>", "\n", clean, flags=re.I)
|
|||
|
|
clean = re.sub(r"</(p|div|li|h\d)>", "\n", clean, flags=re.I)
|
|||
|
|
t = htmlmod.unescape(re.sub(r"<[^>]+>", " ", clean))
|
|||
|
|
t = re.sub(r"[ \t]+", " ", t)
|
|||
|
|
return re.sub(r"\n{3,}", "\n\n", t)
|
|||
|
|
|
|||
|
|
|
|||
|
|
def grab(block: str, start: str, ends: list[str], require_colon: bool = True) -> str:
|
|||
|
|
col = r"\s*:" if require_colon else r"\s*:?"
|
|||
|
|
i = re.search(rf"(?m)(?<![\w]){start}{col}", block)
|
|||
|
|
if not i:
|
|||
|
|
return ""
|
|||
|
|
rest = block[i.end() :]
|
|||
|
|
j = None
|
|||
|
|
for e in ends:
|
|||
|
|
m = re.search(e, rest)
|
|||
|
|
if m and (j is None or m.start() < j):
|
|||
|
|
j = m.start()
|
|||
|
|
chunk = rest[:j] if j is not None else rest[:2500]
|
|||
|
|
keep = []
|
|||
|
|
for ln in chunk.splitlines():
|
|||
|
|
ln = ln.strip()
|
|||
|
|
low = ln.lower()
|
|||
|
|
if any(k in low for k in ("volg ons", "verkrijgbaar bij", "geinteresseerd", "vraag om samples")):
|
|||
|
|
break
|
|||
|
|
if any(k in low for k in ("facebook", "instagram", "copyright", "privacybeleid", "skip to", "share", "print")):
|
|||
|
|
continue
|
|||
|
|
if len(ln) < 2:
|
|||
|
|
continue
|
|||
|
|
keep.append(ln)
|
|||
|
|
return "\n".join(keep).strip()
|
|||
|
|
|
|||
|
|
|
|||
|
|
def harvest_products(url: str) -> list[tuple[str, str, str]]:
|
|||
|
|
html = get(url)
|
|||
|
|
cut = re.split(r"VOLG ONS OP FACEBOOK", html, flags=re.I)[0]
|
|||
|
|
groups: list[tuple[str, str, str]] = []
|
|||
|
|
parts = re.split(r"<h2[^>]*>", cut, flags=re.I)
|
|||
|
|
skip_h2 = ("volg", "verkrijgbaar", "geinteresseerd", "cucina")
|
|||
|
|
for part in parts[1:]:
|
|||
|
|
hm = re.match(r"(.*?)</h2>", part, re.I | re.S)
|
|||
|
|
if not hm:
|
|||
|
|
continue
|
|||
|
|
group = strip_tags(hm.group(1))
|
|||
|
|
if not group or any(s in group.lower() for s in skip_h2):
|
|||
|
|
continue
|
|||
|
|
body = part[hm.end() :]
|
|||
|
|
h3s = [strip_tags(x) for x in re.findall(r"<h3[^>]*>(.*?)</h3>", body, flags=re.I | re.S)]
|
|||
|
|
h3s = [x for x in h3s if x]
|
|||
|
|
imgs = [i.split("?")[0] for i in re.findall(r'<img[^>]+src="([^"]+)"', body)]
|
|||
|
|
skip = ("Group-1000002463", "image-6.png", "image-7.png", "image-8.png", "Group-1000002471", "Ellipse", "Vector-1", "fi_")
|
|||
|
|
imgs = [i for i in imgs if "uploads" in i and not any(s in i for s in skip)]
|
|||
|
|
uniq = list(dict.fromkeys(imgs))
|
|||
|
|
n = min(len(h3s), len(uniq))
|
|||
|
|
for name, img in zip(h3s[:n], uniq[:n]):
|
|||
|
|
groups.append((group, name, img))
|
|||
|
|
if not groups:
|
|||
|
|
h3s = [strip_tags(x) for x in re.findall(r"<h3[^>]*>(.*?)</h3>", cut, flags=re.I | re.S)]
|
|||
|
|
h3s = [x for x in h3s if x]
|
|||
|
|
imgs = [i.split("?")[0] for i in re.findall(r'<img[^>]+src="([^"]+)"', cut)]
|
|||
|
|
skip = ("Group-1000002463", "image-6.png", "image-7.png", "image-8.png", "Group-1000002471", "Ellipse", "Vector-1", "fi_")
|
|||
|
|
imgs = [i for i in imgs if "uploads" in i and not any(s in i for s in skip)]
|
|||
|
|
uniq = list(dict.fromkeys(imgs))
|
|||
|
|
for name, img in zip(h3s, uniq):
|
|||
|
|
groups.append(("", name, img))
|
|||
|
|
return groups
|
|||
|
|
|
|||
|
|
|
|||
|
|
def recipe_for(name: str) -> str | None:
|
|||
|
|
key = name.lower().replace("’", "'").strip()
|
|||
|
|
key = key.replace("'", "")
|
|||
|
|
for k, slug in RECIPE_FOR.items():
|
|||
|
|
kk = k.replace("'", "")
|
|||
|
|
if kk in key or key in kk:
|
|||
|
|
return slug
|
|||
|
|
return None
|
|||
|
|
|
|||
|
|
|
|||
|
|
def esc(s: str) -> str:
|
|||
|
|
return htmlmod.escape(s or "")
|
|||
|
|
|
|||
|
|
|
|||
|
|
def nav(active: str) -> str:
|
|||
|
|
items = [
|
|||
|
|
("/to-cook/", "to-cook", "To Cook"),
|
|||
|
|
("/to-heat/", "to-heat", "To Heat"),
|
|||
|
|
("/to-go/", "to-go", "To Go"),
|
|||
|
|
("/halal-meat/", "halal-meat", "Meat"),
|
|||
|
|
("/recepten/", "recepten", "Recepten"),
|
|||
|
|
("/halal/", "halal", "Halal"),
|
|||
|
|
("/over-ons/", "over-ons", "Over"),
|
|||
|
|
]
|
|||
|
|
links = []
|
|||
|
|
for href, key, label in items:
|
|||
|
|
cur = ' aria-current="page"' if active == key else ""
|
|||
|
|
links.append(f'<a href="{href}"{cur}>{label}</a>')
|
|||
|
|
return "\n ".join(links)
|
|||
|
|
|
|||
|
|
|
|||
|
|
def page(title: str, active: str, main: str, desc: str = "") -> str:
|
|||
|
|
d = desc or title
|
|||
|
|
return f"""<!DOCTYPE html>
|
|||
|
|
<html lang="nl">
|
|||
|
|
<head>
|
|||
|
|
<meta charset="utf-8">
|
|||
|
|
<meta name="viewport" content="width=device-width, initial-scale=1">
|
|||
|
|
<title>{esc(title)}</title>
|
|||
|
|
<meta name="description" content="{esc(d)}">
|
|||
|
|
<link rel="icon" href="{LOGO}">
|
|||
|
|
<link rel="preconnect" href="https://fonts.googleapis.com">
|
|||
|
|
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin>
|
|||
|
|
<link href="https://fonts.googleapis.com/css2?family=Archivo+Black&family=Archivo:ital,wght@0,400;0,600;0,700;1,400&family=Source+Serif+4:opsz,wght@8..60,400;8..60,600&display=swap" rel="stylesheet">
|
|||
|
|
<link rel="stylesheet" href="/css/site.css">
|
|||
|
|
</head>
|
|||
|
|
<body>
|
|||
|
|
<a class="skip" href="#inhoud">Naar de inhoud</a>
|
|||
|
|
<header class="site-header" data-header>
|
|||
|
|
<a class="brand" href="/">
|
|||
|
|
<img src="{LOGO}" width="42" height="42" alt="Cucina Halal Food">
|
|||
|
|
<span class="word">CUCINA</span>
|
|||
|
|
</a>
|
|||
|
|
<button class="nav-toggle" type="button" aria-expanded="false" aria-label="Menu">☰</button>
|
|||
|
|
<nav class="site-nav">
|
|||
|
|
{nav(active)}
|
|||
|
|
<a class="btn" href="/waar-te-koop/">Vind in de buurt</a>
|
|||
|
|
</nav>
|
|||
|
|
</header>
|
|||
|
|
<main id="inhoud">
|
|||
|
|
{main}
|
|||
|
|
</main>
|
|||
|
|
<footer class="site-footer">
|
|||
|
|
<div>
|
|||
|
|
<strong>Cucina Food</strong>
|
|||
|
|
<p>Bos en Lommerplein 280<br>1055 RW Amsterdam<br>
|
|||
|
|
<a href="mailto:info@cucinafood.nl">info@cucinafood.nl</a></p>
|
|||
|
|
<div class="social">
|
|||
|
|
<a href="https://www.facebook.com/Cucinafoodnl/">Facebook</a>
|
|||
|
|
<a href="https://www.instagram.com/cucinafoodnl/">Instagram</a>
|
|||
|
|
<a href="https://www.tiktok.com/@cucinafood">TikTok</a>
|
|||
|
|
</div>
|
|||
|
|
</div>
|
|||
|
|
<div>
|
|||
|
|
<strong>Assortiment</strong>
|
|||
|
|
<ul>
|
|||
|
|
<li><a href="/to-cook/">To Cook</a></li>
|
|||
|
|
<li><a href="/to-heat/">To Heat</a></li>
|
|||
|
|
<li><a href="/to-go/">To Go</a></li>
|
|||
|
|
<li><a href="/halal-meat/">Halal Meat</a></li>
|
|||
|
|
<li><a href="/recepten/">Recepten</a></li>
|
|||
|
|
</ul>
|
|||
|
|
</div>
|
|||
|
|
<div>
|
|||
|
|
<strong>Cucina</strong>
|
|||
|
|
<ul>
|
|||
|
|
<li><a href="/halal/">Halal beleid</a></li>
|
|||
|
|
<li><a href="/over-ons/">Over ons</a></li>
|
|||
|
|
<li><a href="/waar-te-koop/">Waar te koop</a></li>
|
|||
|
|
<li><a href="/contact/">Contact & samples</a></li>
|
|||
|
|
<li><a href="/privacy/">Privacy</a></li>
|
|||
|
|
<li><a href="/voorwaarden/">Voorwaarden</a></li>
|
|||
|
|
</ul>
|
|||
|
|
</div>
|
|||
|
|
</footer>
|
|||
|
|
<script src="/js/site.js"></script>
|
|||
|
|
</body>
|
|||
|
|
</html>
|
|||
|
|
"""
|
|||
|
|
|
|||
|
|
|
|||
|
|
def tile(name: str, img: str, sub: str, href: str | None = None, product: dict | None = None) -> str:
|
|||
|
|
inner = f'<img src="{esc(img)}" alt="{esc(name)}"><b>{esc(name)}</b><span>{esc(sub)}</span>'
|
|||
|
|
if href:
|
|||
|
|
return f'<a class="tile" href="{href}">{inner}</a>'
|
|||
|
|
if product:
|
|||
|
|
payload = esc(json.dumps(product, ensure_ascii=False))
|
|||
|
|
return f'<button class="tile" type="button" data-product=\'{payload}\'>{inner}</button>'
|
|||
|
|
return f'<div class="tile">{inner}</div>'
|
|||
|
|
|
|||
|
|
|
|||
|
|
def drawer() -> str:
|
|||
|
|
return """
|
|||
|
|
<dialog class="drawer" id="product-drawer">
|
|||
|
|
<form method="dialog"><button class="drawer-close" aria-label="Sluiten">×</button></form>
|
|||
|
|
<div class="drawer-body">
|
|||
|
|
<img data-d-img alt="">
|
|||
|
|
<div>
|
|||
|
|
<h2 data-d-name></h2>
|
|||
|
|
<p class="serif" data-d-blurb></p>
|
|||
|
|
<a class="btn" data-d-recipe href="#">Naar het recept</a>
|
|||
|
|
</div>
|
|||
|
|
</div>
|
|||
|
|
</dialog>
|
|||
|
|
"""
|
|||
|
|
|
|||
|
|
|
|||
|
|
def finder_block(compact: bool = False) -> str:
|
|||
|
|
h = "Waar ligt Cucina?" if not compact else "Zoek een winkel"
|
|||
|
|
return f"""
|
|||
|
|
<section class="lime-band" data-finder>
|
|||
|
|
<div class="wrap">
|
|||
|
|
<h2>{h}</h2>
|
|||
|
|
<p class="lede">Zoek op plaats of postcode. Geen muur van 500 adressen.</p>
|
|||
|
|
<form class="search" role="search">
|
|||
|
|
<input type="search" name="q" placeholder="Plaats of postcode" autocomplete="off">
|
|||
|
|
<button type="submit">Zoek</button>
|
|||
|
|
</form>
|
|||
|
|
<p class="muted" data-status>Typ een plaats of postcode.</p>
|
|||
|
|
<ul class="store-list" data-results></ul>
|
|||
|
|
</div>
|
|||
|
|
</section>
|
|||
|
|
"""
|
|||
|
|
|
|||
|
|
|
|||
|
|
def write(path: Path, html: str) -> None:
|
|||
|
|
path.parent.mkdir(parents=True, exist_ok=True)
|
|||
|
|
path.write_text(html, encoding="utf-8")
|
|||
|
|
|
|||
|
|
|
|||
|
|
def ingredients_list(text: str) -> list[str]:
|
|||
|
|
items = []
|
|||
|
|
for ln in text.splitlines():
|
|||
|
|
low = ln.lower()
|
|||
|
|
if low.startswith("ingrediënten") or "aantal personen" in low:
|
|||
|
|
if items:
|
|||
|
|
break
|
|||
|
|
continue
|
|||
|
|
if low.startswith("zelf toevoegen"):
|
|||
|
|
continue
|
|||
|
|
items.append(ln)
|
|||
|
|
return items
|
|||
|
|
|
|||
|
|
|
|||
|
|
def build() -> None:
|
|||
|
|
data = ROOT / "data"
|
|||
|
|
data.mkdir(exist_ok=True)
|
|||
|
|
stores = json.loads(Path("/tmp/cucina-stores.json").read_text())
|
|||
|
|
(data / "stores.json").write_text(json.dumps(stores, ensure_ascii=False), encoding="utf-8")
|
|||
|
|
|
|||
|
|
recipes_raw = json.loads(Path("/tmp/cucina-recipes.json").read_text())
|
|||
|
|
# Re-parse sections with word-boundary fix if cook looks like prep
|
|||
|
|
recipes = []
|
|||
|
|
for r in recipes_raw:
|
|||
|
|
# We already have fields; fix cook if it equals prep
|
|||
|
|
html = get("https://www.cucinafood.nl/" + r["slug"] + "/")
|
|||
|
|
t = textify(html)
|
|||
|
|
r["prep"] = grab(t, "Voorbereiding", [r"(?<![\w])Bereiding\s*:", r"Serveren", r"Ingrediënten"])
|
|||
|
|
r["cook"] = grab(t, "Bereiding", [r"Serveren", r"Ingrediënten"])
|
|||
|
|
r["serve"] = grab(t, "Serveren", [r"Ingrediënten", r"VOLG ONS"], require_colon=False)
|
|||
|
|
r["ingredients"] = grab(t, "Ingrediënten", [r"VOLG ONS", r"Cucina is"])
|
|||
|
|
recipes.append(r)
|
|||
|
|
(data / "recipes.json").write_text(json.dumps(recipes, ensure_ascii=False, indent=2), encoding="utf-8")
|
|||
|
|
rec_by = {r["slug"]: r for r in recipes}
|
|||
|
|
|
|||
|
|
products = {}
|
|||
|
|
for slug, meta in LINE_META.items():
|
|||
|
|
pairs = harvest_products(f"https://www.cucinafood.nl/{slug}/")
|
|||
|
|
groups: dict[str, list] = {}
|
|||
|
|
order = []
|
|||
|
|
for group, name, img in pairs:
|
|||
|
|
g = group or meta["name"]
|
|||
|
|
if g not in groups:
|
|||
|
|
groups[g] = []
|
|||
|
|
order.append(g)
|
|||
|
|
groups[g].append({
|
|||
|
|
"name": name,
|
|||
|
|
"image": img,
|
|||
|
|
"blurb": meta["blurb"],
|
|||
|
|
"recipe": recipe_for(name),
|
|||
|
|
})
|
|||
|
|
products[slug] = {"meta": meta, "groups": [(g, groups[g]) for g in order]}
|
|||
|
|
print(slug, "products", sum(len(v) for v in groups.values()), "groups", order)
|
|||
|
|
|
|||
|
|
(data / "products.json").write_text(json.dumps(products, ensure_ascii=False, indent=2), encoding="utf-8")
|
|||
|
|
|
|||
|
|
# HOME
|
|||
|
|
featured = recipes[:3] if len(recipes) >= 3 else recipes
|
|||
|
|
# pick pasta pesto, kokoscurry, teriyaki if present
|
|||
|
|
want = [
|
|||
|
|
"rode-pasta-pesto-met-gehaktballetjes",
|
|||
|
|
"gele-kokoscurry-met-kip",
|
|||
|
|
"kip-teriyaki-met-gewokte-broccoli",
|
|||
|
|
]
|
|||
|
|
featured = [rec_by[s] for s in want if s in rec_by] or featured
|
|||
|
|
|
|||
|
|
home_tiles = "".join(
|
|||
|
|
tile(LINE_META[s]["name"], LINE_META[s]["hero"], LINE_META[s]["kicker"], f"/{s}/")
|
|||
|
|
for s in ("to-cook", "to-heat", "to-go", "halal-meat")
|
|||
|
|
)
|
|||
|
|
rec_tiles = "".join(
|
|||
|
|
tile(r["title"], r["image"], f'{r["time"]} · {r["serves"]}', f'/recepten/{r["slug"]}/')
|
|||
|
|
for r in featured
|
|||
|
|
)
|
|||
|
|
home = f"""
|
|||
|
|
<section class="hero">
|
|||
|
|
<img class="bg" src="{CDN}/2025/02/Chicken-Meat-Kababs-1.webp" alt="Halal gerechten van Cucina">
|
|||
|
|
<div class="hero-copy">
|
|||
|
|
<div class="kicker">Halal · supermarkt · vandaag op tafel</div>
|
|||
|
|
<h1>De wereldkeuken, gewoon in het schap.</h1>
|
|||
|
|
<p>Heerlijke halal producten om mee te koken, op te warmen of to go. Cucina is de keuken van ons allemaal.</p>
|
|||
|
|
<div class="hero-actions">
|
|||
|
|
<a class="btn" href="/to-cook/">Bekijk To Cook</a>
|
|||
|
|
<a class="btn btn-ghost" href="/waar-te-koop/">Vind in de buurt</a>
|
|||
|
|
</div>
|
|||
|
|
</div>
|
|||
|
|
</section>
|
|||
|
|
<section class="section">
|
|||
|
|
<div class="wrap">
|
|||
|
|
<h2>Vier manieren om te eten</h2>
|
|||
|
|
<p class="lede">Koken, opwarmen, meenemen of puur vlees. Altijd halal, altijd in de supermarkt.</p>
|
|||
|
|
<div class="grid-4">{home_tiles}</div>
|
|||
|
|
</div>
|
|||
|
|
</section>
|
|||
|
|
<section class="section" style="padding-top:0">
|
|||
|
|
<div class="wrap">
|
|||
|
|
<h2>Vanavond op tafel</h2>
|
|||
|
|
<div class="grid-3">{rec_tiles}</div>
|
|||
|
|
<p style="margin-top:1rem"><a class="btn btn-ghost dark" href="/recepten/">Alle recepten</a></p>
|
|||
|
|
</div>
|
|||
|
|
</section>
|
|||
|
|
{finder_block()}
|
|||
|
|
<section class="section">
|
|||
|
|
<div class="wrap">
|
|||
|
|
<h2>Halal, van bron tot bord</h2>
|
|||
|
|
<div class="trust">
|
|||
|
|
<div><strong>100% halal</strong>Opgericht door moslimondernemers met Marokkaanse roots. Halal is geen label, het is hoe we werken. In lijn met SMIIC-normen.</div>
|
|||
|
|
<div><strong>BRC & IFS</strong>Geproduceerd onder internationale voedselveiligheidsstandaarden, met controle op elke stap.</div>
|
|||
|
|
<div><strong>Gewoon in de supermarkt</strong>Halal food hoort in elk schap te liggen — zichtbaar, vertrouwd, zonder concessies.</div>
|
|||
|
|
</div>
|
|||
|
|
</div>
|
|||
|
|
</section>
|
|||
|
|
<section class="section" style="padding-top:0">
|
|||
|
|
<div class="wrap">
|
|||
|
|
<h2>Verkrijgbaar bij</h2>
|
|||
|
|
<div class="retail">
|
|||
|
|
<img src="{CDN}/2025/02/image-6.png" alt="Retailpartner">
|
|||
|
|
<img src="{CDN}/2025/02/image-7.png" alt="Retailpartner">
|
|||
|
|
<img src="{CDN}/2025/02/image-8.png" alt="Retailpartner">
|
|||
|
|
<img src="{CDN}/2025/02/Group-1000002471.svg" alt="Retailpartner">
|
|||
|
|
</div>
|
|||
|
|
</div>
|
|||
|
|
</section>
|
|||
|
|
<section class="section" style="padding-top:0">
|
|||
|
|
<div class="wrap">
|
|||
|
|
<div class="samples">
|
|||
|
|
<div>
|
|||
|
|
<h2>Voor retailers</h2>
|
|||
|
|
<p class="lede" style="margin:0">Wil je Cucina in het schap? Vraag samples aan. Shop je voor vanavond, gebruik de winkelvinder.</p>
|
|||
|
|
</div>
|
|||
|
|
<a class="btn" href="/contact/#samples">Vraag samples</a>
|
|||
|
|
</div>
|
|||
|
|
</div>
|
|||
|
|
</section>
|
|||
|
|
"""
|
|||
|
|
write(ROOT / "index.html", page(
|
|||
|
|
"Cucina Food — halal wereldkeuken in de supermarkt",
|
|||
|
|
"home",
|
|||
|
|
home,
|
|||
|
|
"Halal maaltijdpakketten, ovenschotels, salades en vlees. Vind Cucina in een supermarkt bij jou in de buurt.",
|
|||
|
|
))
|
|||
|
|
|
|||
|
|
for slug, pack in products.items():
|
|||
|
|
meta = pack["meta"]
|
|||
|
|
blocks = []
|
|||
|
|
for gname, items in pack["groups"]:
|
|||
|
|
cards = "".join(
|
|||
|
|
tile(p["name"], p["image"], p["blurb"], product=p) for p in items
|
|||
|
|
)
|
|||
|
|
title = f"<h3>{esc(gname)}</h3>" if gname and gname.lower() not in meta["name"].lower() else ""
|
|||
|
|
blocks.append(f'<div class="group">{title}<div class="grid-3">{cards}</div></div>')
|
|||
|
|
points = "".join(f"<li>{esc(p)}</li>" for p in meta["points"])
|
|||
|
|
main = f"""
|
|||
|
|
<section class="hero page-hero">
|
|||
|
|
<img class="bg" src="{meta['hero']}" alt="{esc(meta['name'])}">
|
|||
|
|
<div class="hero-copy">
|
|||
|
|
<div class="kicker">{esc(meta['kicker'])}</div>
|
|||
|
|
<h1>{esc(meta['name'])}</h1>
|
|||
|
|
<p>{esc(meta['intro'])}</p>
|
|||
|
|
<div class="hero-actions">
|
|||
|
|
<a class="btn" href="/waar-te-koop/">Vind in de buurt</a>
|
|||
|
|
</div>
|
|||
|
|
</div>
|
|||
|
|
</section>
|
|||
|
|
<section class="section">
|
|||
|
|
<div class="wrap">
|
|||
|
|
<ul class="serif">{points}</ul>
|
|||
|
|
{''.join(blocks)}
|
|||
|
|
</div>
|
|||
|
|
</section>
|
|||
|
|
{drawer()}
|
|||
|
|
"""
|
|||
|
|
write(ROOT / slug / "index.html", page(
|
|||
|
|
f"{meta['name']} — Cucina Food",
|
|||
|
|
slug,
|
|||
|
|
main,
|
|||
|
|
meta["intro"],
|
|||
|
|
))
|
|||
|
|
|
|||
|
|
rec_cards = "".join(
|
|||
|
|
tile(r["title"], r["image"], f'{r["time"]} · {r["serves"]}', f'/recepten/{r["slug"]}/')
|
|||
|
|
for r in recipes
|
|||
|
|
)
|
|||
|
|
write(ROOT / "recepten" / "index.html", page(
|
|||
|
|
"Recepten — Cucina Food",
|
|||
|
|
"recepten",
|
|||
|
|
f"""
|
|||
|
|
<section class="section">
|
|||
|
|
<div class="wrap">
|
|||
|
|
<p class="crumbs"><a href="/">Home</a> / Recepten</p>
|
|||
|
|
<h1>Recepten</h1>
|
|||
|
|
<p class="lede">Duik in de keuken met Cucina. Snelle maaltijden en wereldse smaken, gemaakt om te delen met #CucinaFood.</p>
|
|||
|
|
<div class="grid-3">{rec_cards}</div>
|
|||
|
|
</div>
|
|||
|
|
</section>
|
|||
|
|
""",
|
|||
|
|
"Halal recepten bij de Cucina maaltijdpakketten.",
|
|||
|
|
))
|
|||
|
|
|
|||
|
|
for r in recipes:
|
|||
|
|
ings = "".join(f"<li>{esc(x)}</li>" for x in ingredients_list(r.get("ingredients") or ""))
|
|||
|
|
def paras(text: str) -> str:
|
|||
|
|
return "".join(f"<p>{esc(p)}</p>" for p in (text or "").split("\n") if p.strip())
|
|||
|
|
main = f"""
|
|||
|
|
<section class="section">
|
|||
|
|
<div class="wrap recipe-layout">
|
|||
|
|
<div class="recipe-hero">
|
|||
|
|
<p class="crumbs"><a href="/">Home</a> / <a href="/recepten/">Recepten</a></p>
|
|||
|
|
<img src="{esc(r['image'])}" alt="{esc(r['title'])}">
|
|||
|
|
</div>
|
|||
|
|
<div>
|
|||
|
|
<div class="kicker">To Cook</div>
|
|||
|
|
<h1>{esc(r['title'])}</h1>
|
|||
|
|
<div class="meta-row"><span>{esc(r['time'])}</span><span>{esc(r['serves'])}</span></div>
|
|||
|
|
<div class="ings">
|
|||
|
|
<h3>Ingrediënten</h3>
|
|||
|
|
<ul>{ings}</ul>
|
|||
|
|
</div>
|
|||
|
|
</div>
|
|||
|
|
<div class="steps" style="grid-column:1/-1">
|
|||
|
|
<h3>Voorbereiding</h3>{paras(r.get('prep') or '')}
|
|||
|
|
<h3>Bereiding</h3>{paras(r.get('cook') or '')}
|
|||
|
|
<h3>Serveren</h3>{paras(r.get('serve') or '')}
|
|||
|
|
<p><a class="btn" href="/to-cook/">Naar To Cook</a></p>
|
|||
|
|
</div>
|
|||
|
|
</div>
|
|||
|
|
</section>
|
|||
|
|
"""
|
|||
|
|
write(ROOT / "recepten" / r["slug"] / "index.html", page(f"{r['title']} — Cucina Food", "recepten", main, r["title"]))
|
|||
|
|
|
|||
|
|
write(ROOT / "waar-te-koop" / "index.html", page(
|
|||
|
|
"Waar te koop — Cucina Food",
|
|||
|
|
"waar-te-koop",
|
|||
|
|
f"""
|
|||
|
|
<section class="section" style="padding-bottom:0">
|
|||
|
|
<div class="wrap">
|
|||
|
|
<p class="crumbs"><a href="/">Home</a> / Waar te koop</p>
|
|||
|
|
<h1>Waar ligt Cucina?</h1>
|
|||
|
|
<p class="lede">Cucina ligt in supermarkten in Nederland en België. Zoek op plaats of postcode.</p>
|
|||
|
|
</div>
|
|||
|
|
</section>
|
|||
|
|
{finder_block()}
|
|||
|
|
<section class="section">
|
|||
|
|
<div class="wrap">
|
|||
|
|
<h2>Verkrijgbaar bij</h2>
|
|||
|
|
<div class="retail">
|
|||
|
|
<img src="{CDN}/2025/02/image-6.png" alt="Retailpartner">
|
|||
|
|
<img src="{CDN}/2025/02/image-7.png" alt="Retailpartner">
|
|||
|
|
<img src="{CDN}/2025/02/image-8.png" alt="Retailpartner">
|
|||
|
|
<img src="{CDN}/2025/02/Group-1000002471.svg" alt="Retailpartner">
|
|||
|
|
</div>
|
|||
|
|
</div>
|
|||
|
|
</section>
|
|||
|
|
""",
|
|||
|
|
"Vind Cucina Food in een supermarkt bij jou in de buurt.",
|
|||
|
|
))
|
|||
|
|
|
|||
|
|
write(ROOT / "over-ons" / "index.html", page(
|
|||
|
|
"Over ons — Cucina Food",
|
|||
|
|
"over-ons",
|
|||
|
|
f"""
|
|||
|
|
<section class="hero page-hero">
|
|||
|
|
<img class="bg" src="{CDN}/2025/10/cucina-food.webp" alt="Cucina Food">
|
|||
|
|
<div class="hero-copy">
|
|||
|
|
<div class="kicker">Ons verhaal</div>
|
|||
|
|
<h1>Over Cucina</h1>
|
|||
|
|
<p>Opgericht door moslimondernemers, voor de halalconsument — en voor iedereen die van de wereldkeuken houdt.</p>
|
|||
|
|
</div>
|
|||
|
|
</section>
|
|||
|
|
<section class="section">
|
|||
|
|
<div class="wrap prose">
|
|||
|
|
<p>Cucina betekent ‘keuken’, en dat is precies wat we willen zijn: de keuken van ons allemaal. Een plek waar smaken uit de wereld samenkomen en waar traditie en vernieuwing elkaar ontmoeten.</p>
|
|||
|
|
<p>Cucina Food is een halal voedingsbedrijf. Ons team zet zich in om de belangen van de halalconsument te behartigen en hen te voorzien van zowel vertrouwde als innovatieve halal gemaksproducten. We nodigen iedereen uit om mee te genieten van de kleurrijke wereldkeuken.</p>
|
|||
|
|
<h2>Visie</h2>
|
|||
|
|
<p>Halalproducten net zo vanzelfsprekend als elke andere keuze in het schap. Authenticiteit van de wereldkeuken, gecombineerd met gemak, smaak en kwaliteit — passend bij een moderne levensstijl.</p>
|
|||
|
|
<h2>Missie</h2>
|
|||
|
|
<p>We maken halal gemaksproducten toegankelijk en zichtbaar in de supermarkt. Tegelijkertijd bewaken we de halalwaardigheid van bron tot bord, zodat je met een gerust hart kiest bij je vertrouwde winkel.</p>
|
|||
|
|
<p>We produceren onder strikte voedselveiligheidsvoorschriften, zoals BRC en IFS.</p>
|
|||
|
|
<p><a class="btn" href="/halal/">Lees het halalbeleid</a></p>
|
|||
|
|
</div>
|
|||
|
|
</section>
|
|||
|
|
""",
|
|||
|
|
"Cucina Food: halal gemak, opgericht door moslimondernemers.",
|
|||
|
|
))
|
|||
|
|
|
|||
|
|
write(ROOT / "halal" / "index.html", page(
|
|||
|
|
"Halal beleid — Cucina Food",
|
|||
|
|
"halal",
|
|||
|
|
"""
|
|||
|
|
<section class="section">
|
|||
|
|
<div class="wrap prose">
|
|||
|
|
<p class="crumbs"><a href="/">Home</a> / Halal beleid</p>
|
|||
|
|
<div class="kicker">100% halal</div>
|
|||
|
|
<h1>Halalwaardigheid</h1>
|
|||
|
|
<p>Cucina Food is opgericht en wordt geleid door islamitische ondernemers met Marokkaanse roots. Voor ons is halal geen trend of label, maar een manier van leven die in elke stap van het bedrijf zichtbaar is.</p>
|
|||
|
|
<p>Ons beleid is helder en streng: elk product is volledig halal. Voor kip, rund, lam en kalf werken we uitsluitend met methoden die voldoen aan erkende halalnormen, in lijn met de standaarden van het internationale instituut SMIIC.</p>
|
|||
|
|
<h2>Certificering</h2>
|
|||
|
|
<p>Onze kwaliteitsafdeling controleert elke stap in het productieproces en werkt samen met erkende halalcertificeringsinstanties. We produceren in een schone, gecontroleerde omgeving volgens BRC en IFS.</p>
|
|||
|
|
<p>Dankzij dat toezicht kun je met een gerust hart genieten van elk Cucina-product.</p>
|
|||
|
|
<p><a class="btn" href="/waar-te-koop/">Vind in de buurt</a></p>
|
|||
|
|
</div>
|
|||
|
|
</section>
|
|||
|
|
""",
|
|||
|
|
"Cucina is 100% halal, in lijn met SMIIC, BRC en IFS.",
|
|||
|
|
))
|
|||
|
|
|
|||
|
|
write(ROOT / "contact" / "index.html", page(
|
|||
|
|
"Contact — Cucina Food",
|
|||
|
|
"contact",
|
|||
|
|
"""
|
|||
|
|
<section class="section">
|
|||
|
|
<div class="wrap">
|
|||
|
|
<p class="crumbs"><a href="/">Home</a> / Contact</p>
|
|||
|
|
<h1>Neem contact op</h1>
|
|||
|
|
<p class="lede">Vragen, schapinformatie of samples voor retailers. We reageren via e-mail.</p>
|
|||
|
|
<div class="grid-2">
|
|||
|
|
<form class="form" data-mailto>
|
|||
|
|
<input type="hidden" name="onderwerp" value="Bericht via cucinafood.nl">
|
|||
|
|
<label>Naam <input name="Naam" required autocomplete="name"></label>
|
|||
|
|
<label>Telefoon <input name="Telefoon" type="tel" autocomplete="tel"></label>
|
|||
|
|
<label>E-mail <input name="E-mail" type="email" required autocomplete="email"></label>
|
|||
|
|
<label>Bericht <textarea name="Bericht" required></textarea></label>
|
|||
|
|
<button class="btn" type="submit">Verstuur</button>
|
|||
|
|
</form>
|
|||
|
|
<div class="prose">
|
|||
|
|
<p><strong>Cucina Food</strong><br>Bos en Lommerplein 280<br>1055 RW Amsterdam</p>
|
|||
|
|
<p><a href="mailto:info@cucinafood.nl">info@cucinafood.nl</a></p>
|
|||
|
|
<p>Shop je als consument? <a href="/waar-te-koop/">Vind Cucina in de buurt</a>.</p>
|
|||
|
|
</div>
|
|||
|
|
</div>
|
|||
|
|
<div class="samples" id="samples" style="margin-top:2.5rem">
|
|||
|
|
<div>
|
|||
|
|
<h2>Samples voor retailers</h2>
|
|||
|
|
<p class="lede" style="margin:0 0 1rem">Geïnteresseerd in Cucina in het schap? Stuur je gegevens. We nemen contact op.</p>
|
|||
|
|
<form class="form" data-mailto>
|
|||
|
|
<input type="hidden" name="onderwerp" value="Sample-aanvraag Cucina Food">
|
|||
|
|
<label>Naam <input name="Naam" required></label>
|
|||
|
|
<label>Bedrijfsnaam <input name="Bedrijf" required></label>
|
|||
|
|
<label>Telefoon <input name="Telefoon" type="tel"></label>
|
|||
|
|
<label>E-mail <input name="E-mail" type="email" required></label>
|
|||
|
|
<label>Bericht <textarea name="Bericht" placeholder="Welke lijn, welke regio?"></textarea></label>
|
|||
|
|
<button class="btn" type="submit">Vraag samples</button>
|
|||
|
|
</form>
|
|||
|
|
</div>
|
|||
|
|
</div>
|
|||
|
|
</div>
|
|||
|
|
</section>
|
|||
|
|
""",
|
|||
|
|
"Contact en sample-aanvragen voor Cucina Food.",
|
|||
|
|
))
|
|||
|
|
|
|||
|
|
write(ROOT / "privacy" / "index.html", page(
|
|||
|
|
"Privacybeleid — Cucina Food",
|
|||
|
|
"",
|
|||
|
|
"""
|
|||
|
|
<section class="section"><div class="wrap prose">
|
|||
|
|
<h1>Privacybeleid</h1>
|
|||
|
|
<p>Deze statische website slaat zelf geen accounts of bestellingen op. Als je het contact- of sampleformulier gebruikt, opent je e-mailprogramma een bericht naar <a href="mailto:info@cucinafood.nl">info@cucinafood.nl</a> met de gegevens die je invult (naam, e-mail, telefoon, bericht, eventueel bedrijfsnaam).</p>
|
|||
|
|
<p>We gebruiken die gegevens alleen om je vraag te beantwoorden of samples te bespreken. We verkopen ze niet.</p>
|
|||
|
|
<p>De productfoto’s en het logo komen van de bestaande Cucina-omgeving. Winkellocaties zijn overgenomen van cucinafood.nl zodat je een winkel kunt zoeken; die adressen zijn openbare winkeladressen.</p>
|
|||
|
|
<p>Vragen: <a href="mailto:info@cucinafood.nl">info@cucinafood.nl</a>, Bos en Lommerplein 280, 1055 RW Amsterdam.</p>
|
|||
|
|
</div></section>
|
|||
|
|
""",
|
|||
|
|
))
|
|||
|
|
write(ROOT / "voorwaarden" / "index.html", page(
|
|||
|
|
"Voorwaarden — Cucina Food",
|
|||
|
|
"",
|
|||
|
|
"""
|
|||
|
|
<section class="section"><div class="wrap prose">
|
|||
|
|
<h1>Voorwaarden</h1>
|
|||
|
|
<p>Deze website is een merksite. Je koopt hier niet. Producten zijn verkrijgbaar in de supermarkt; beschikbaarheid verschilt per winkel.</p>
|
|||
|
|
<p>Teksten en beeld van het merk Cucina Food. Neem contact op voor samples of schapvragen via <a href="/contact/">contact</a>.</p>
|
|||
|
|
</div></section>
|
|||
|
|
""",
|
|||
|
|
))
|
|||
|
|
|
|||
|
|
print("pages written")
|
|||
|
|
|
|||
|
|
|
|||
|
|
if __name__ == "__main__":
|
|||
|
|
build()
|