SPB Git forge

spb/fabri-ka

Public

Agrégateur de produits québécois — www.fabri-ka.com

217commits 1branches 0releases
66.1 MBsize
maindefault branch
3 h agolast push
Python 38.4% HTML 30.3% TypeScript 17.2% CSS 11% JavaScript 3.2%

Revert "feat(portée Canada): Fabri-Ka devient l'agrégateur des boutiques canadiennes, plus seulement québécoises."

This reverts commit 8223706e162d2496adcf7f003306689e191a026b.
Simon-Pierre Boucher committed 29 days ago (Aug 28, 2026) parent 8223706

41 changed files +130 −208

modified README.md +4 −4
@@ -1,10 +1,10 @@
1 1 <p align="center">
2 − <a href="https://www.fabri-ka.com"><img src="https://www.fabri-ka.com/og.png" width="760" alt="Fabri·Ka — Tous les produits canadiens. Un seul endroit."></a>
2 + <a href="https://www.fabri-ka.com"><img src="https://www.fabri-ka.com/og.png" width="760" alt="Fabri·Ka — Tous les produits québécois. Un seul endroit."></a>
3 3 </p>
4 4
5 5 <h1 align="center">Fabri·Ka</h1>
6 6
7 −<p align="center"><b>Tous les produits canadiens. Un seul endroit.</b><br><a href="https://www.fabri-ka.com">www.fabri-ka.com</a></p>
7 +<p align="center"><b>Tous les produits québécois. Un seul endroit.</b><br><a href="https://www.fabri-ka.com">www.fabri-ka.com</a></p>
8 8
9 9 <div align="center">
10 10
@@ -43,9 +43,9 @@
43 43
44 44 ## Description
45 45
46 −**Fabri-Ka** est le **répertoire des produits canadiens** : un agrégateur de découverte qui recense **385 308 produits** (au 2026-08-24) provenant de **1 582 boutiques en ligne** du Canada — le Québec couvert à la région administrative (17 régions), le reste du pays à la province/territoire. Chaque produit renvoie vers la **boutique d'origine** — **aucune vente sur Fabri-Ka** : les prix, photos et stocks appartiennent aux marchands sources.
46 +**Fabri-Ka** est le **répertoire des produits québécois** : un agrégateur de découverte qui recense **385 308 produits** (au 2026-08-24) provenant de **1 582 boutiques en ligne** du Québec, couvrant les **17 régions administratives** de la province. Chaque produit renvoie vers la **boutique d'origine** — **aucune vente sur Fabri-Ka** : les prix, photos et stocks appartiennent aux marchands sources.
47 47
48 −Acheter local demande de connaître des centaines de boutiques dispersées — érablières, microbrasseries, savonneries, ébénistes, torréfacteurs, fermes… **Fabri-Ka retourne le problème** : un pipeline de découverte recense les boutiques en ligne du Canada (**4 082 boutiques vérifiées** au registre), des connecteurs par *plateforme* aspirent leurs catalogues publics complets, et un **moteur de recherche unique** les rend trouvables — filtrés par **catégorie**, **région** et **origine de fabrication** (classes **A–E**).
48 +Acheter québécois demande de connaître des centaines de boutiques dispersées — érablières, microbrasseries, savonneries, ébénistes, torréfacteurs, fermes… **Fabri-Ka retourne le problème** : un pipeline de découverte recense les boutiques en ligne du Québec (**4 082 boutiques vérifiées** au registre), des connecteurs par *plateforme* aspirent leurs catalogues publics complets, et un **moteur de recherche unique** les rend trouvables — filtrés par **catégorie**, **région** et **origine de fabrication** (classes **A–E**).
49 49
50 50 > Contrairement à un agrégateur classique (1 connecteur / site), Fabri-Ka utilise **1 connecteur / plateforme ecommerce** : le même code **Shopify** sert des centaines de boutiques, piloté par le registre `data/stores.json` produit par la recherche.
51 51
modified fabrika/__init__.py +1 −1
@@ -1,2 +1,2 @@
1 −# Fabri-Ka — Agrégateur de produits canadiens
1 +# Fabri-Ka — Agrégateur de produits québécois
2 2 # Auteur : Simon-Pierre Boucher — contact@spboucher.ai
modified fabrika/auth.py +1 −1
@@ -1,5 +1,5 @@
1 1 # -----------------------------------------------------------------------------
2 −# Fabri-Ka — Agrégateur de produits canadiens
2 +# Fabri-Ka — Agrégateur de produits québécois
3 3 # Auteur : Simon-Pierre Boucher — contact@spboucher.ai
4 4 # auth.py : « Se connecter avec KA ID » — SSO du Groupe KA (hub groupe-ka.com)
5 5 #
modified fabrika/connectors/__init__.py +1 −1
@@ -1,5 +1,5 @@
1 1 # -----------------------------------------------------------------------------
2 −# Fabri-Ka — Agrégateur de produits canadiens
2 +# Fabri-Ka — Agrégateur de produits québécois
3 3 # Auteur : Simon-Pierre Boucher — contact@spboucher.ai
4 4 # connectors/__init__.py : dispatch plateforme -> connecteur générique
5 5 # -----------------------------------------------------------------------------
modified fabrika/connectors/base.py +1 −1
@@ -1,5 +1,5 @@
1 1 # -----------------------------------------------------------------------------
2 −# Fabri-Ka — Agrégateur de produits canadiens
2 +# Fabri-Ka — Agrégateur de produits québécois
3 3 # Auteur : Simon-Pierre Boucher — contact@spboucher.ai
4 4 # connectors/base.py : connecteur générique par plateforme ecommerce.
5 5 # Contrairement à Lou-Ka (1 connecteur / site), Fabri-Ka utilise
modified fabrika/connectors/generic.py +1 −1
@@ -1,5 +1,5 @@
1 1 # -----------------------------------------------------------------------------
2 −# Fabri-Ka — Agrégateur de produits canadiens
2 +# Fabri-Ka — Agrégateur de produits québécois
3 3 # Auteur : Simon-Pierre Boucher — contact@spboucher.ai
4 4 # connectors/generic.py : connecteur universel par sitemap + extraction du
5 5 # balisage produit rendu côté serveur (JSON-LD schema.org Product, microdata,
modified fabrika/connectors/immunotec.py +1 −1
@@ -1,5 +1,5 @@
1 1 # -----------------------------------------------------------------------------
2 −# Fabri-Ka — Agrégateur de produits canadiens
2 +# Fabri-Ka — Agrégateur de produits québécois
3 3 # Auteur : Simon-Pierre Boucher — contact@spboucher.ai
4 4 # connectors/immunotec.py : storefront sur mesure Immunotec (Vaudreuil-Dorion) —
5 5 # Next.js + backend Exigo (scus-back2.immunotec.com). Le site et son API sont
modified fabrika/connectors/scrapfly.py +1 −1
@@ -1,5 +1,5 @@
1 1 # -----------------------------------------------------------------------------
2 −# Fabri-Ka — Agrégateur de produits canadiens
2 +# Fabri-Ka — Agrégateur de produits québécois
3 3 # Auteur : Simon-Pierre Boucher — contact@spboucher.ai
4 4 # connectors/scrapfly.py : transport de secours via l'API Scrapfly (anti-bot,
5 5 # rendu JS optionnel). Utilisé quand l'accès direct échoue (403/429/HTML
modified fabrika/connectors/shopify.py +1 −1
@@ -1,5 +1,5 @@
1 1 # -----------------------------------------------------------------------------
2 −# Fabri-Ka — Agrégateur de produits canadiens
2 +# Fabri-Ka — Agrégateur de produits québécois
3 3 # Auteur : Simon-Pierre Boucher — contact@spboucher.ai
4 4 # connectors/shopify.py : connecteur générique Shopify — catalogue complet
5 5 # via l'endpoint public /products.json (paginé, 250 produits/page).
modified fabrika/connectors/square.py +1 −1
@@ -1,5 +1,5 @@
1 1 # -----------------------------------------------------------------------------
2 −# Fabri-Ka — Agrégateur de produits canadiens
2 +# Fabri-Ka — Agrégateur de produits québécois
3 3 # Auteur : Simon-Pierre Boucher — contact@spboucher.ai
4 4 # connectors/square.py : connecteur générique Square Online (Weebly).
5 5 # Les sites Square Online exposent une API JSON publique côté storefront :
modified fabrika/connectors/squarespace.py +1 −1
@@ -1,5 +1,5 @@
1 1 # -----------------------------------------------------------------------------
2 −# Fabri-Ka — Agrégateur de produits canadiens
2 +# Fabri-Ka — Agrégateur de produits québécois
3 3 # Auteur : Simon-Pierre Boucher — contact@spboucher.ai
4 4 # connectors/squarespace.py : connecteur générique Squarespace Commerce —
5 5 # pages collection en JSON natif (?format=json).
modified fabrika/connectors/votresite.py +1 −1
@@ -1,5 +1,5 @@
1 1 # -----------------------------------------------------------------------------
2 −# Fabri-Ka — Agrégateur de produits canadiens
2 +# Fabri-Ka — Agrégateur de produits québécois
3 3 # Auteur : Simon-Pierre Boucher — contact@spboucher.ai
4 4 # connectors/votresite.py : connecteur Votresite.ca (plateforme québécoise).
5 5 # Les sites Votresite.ca sont des Drupal 8 gérés (thème owebo-votresite) avec
modified fabrika/connectors/woocommerce.py +1 −1
@@ -1,5 +1,5 @@
1 1 # -----------------------------------------------------------------------------
2 −# Fabri-Ka — Agrégateur de produits canadiens
2 +# Fabri-Ka — Agrégateur de produits québécois
3 3 # Auteur : Simon-Pierre Boucher — contact@spboucher.ai
4 4 # connectors/woocommerce.py : connecteur générique WooCommerce — catalogue
5 5 # complet via la Store API publique /wp-json/wc/store/v1/products.
modified fabrika/db.py +1 −1
@@ -1,5 +1,5 @@
1 1 # -----------------------------------------------------------------------------
2 −# Fabri-Ka — Agrégateur de produits canadiens
2 +# Fabri-Ka — Agrégateur de produits québécois
3 3 # Auteur : Simon-Pierre Boucher — contact@spboucher.ai
4 4 # db.py : persistance SQLite — upsert avec hash de contenu, cycle de vie
5 5 # avec délai de grâce, FTS5 pour la recherche plein-texte.
modified fabrika/hubfav.py +1 −1
@@ -1,5 +1,5 @@
1 1 # -----------------------------------------------------------------------------
2 −# Fabri-Ka — Agrégateur de produits canadiens
2 +# Fabri-Ka — Agrégateur de produits québécois
3 3 # Auteur : Simon-Pierre Boucher — contact@spboucher.ai
4 4 # hubfav.py : favoris « Mon univers Ka » — magasin central du Groupe KA.
5 5 # AUCUN stockage local : chaque ♥ est poussé/lu au hub (groupe-ka.com),
modified fabrika/hubprofile.py +1 −1
@@ -1,5 +1,5 @@
1 1 # -----------------------------------------------------------------------------
2 −# Fabri-Ka — Agrégateur de produits canadiens
2 +# Fabri-Ka — Agrégateur de produits québécois
3 3 # Auteur : Simon-Pierre Boucher — contact@spboucher.ai
4 4 # hubprofile.py : profil membre lu depuis le HUB Groupe KA (groupe-ka.com)
5 5 # Le hub est LA source de vérité du profil (bio, ville, emploi, entreprise,
modified fabrika/ingest.py +1 −1
@@ -1,5 +1,5 @@
1 1 # -----------------------------------------------------------------------------
2 −# Fabri-Ka — Agrégateur de produits canadiens
2 +# Fabri-Ka — Agrégateur de produits québécois
3 3 # Auteur : Simon-Pierre Boucher — contact@spboucher.ai
4 4 # ingest.py : orchestration des synchronisations — registre -> connecteurs ->
5 5 # diff BD. Parallélisé par boutique (thread pool), tolérant aux
modified fabrika/inscriptions.py +1 −1
@@ -1,5 +1,5 @@
1 1 # -----------------------------------------------------------------------------
2 −# Fabri-Ka — Agrégateur de produits canadiens
2 +# Fabri-Ka — Agrégateur de produits québécois
3 3 # Auteur : Simon-Pierre Boucher — contact@spboucher.ai
4 4 # inscriptions.py : canal d'auto-inscription des artisans/fabricants
5 5 # (« Inscrire mon atelier »).
modified fabrika/report.py +7 −7
@@ -1,8 +1,8 @@
1 1 # -----------------------------------------------------------------------------
2 −# Fabri-Ka — Agrégateur de produits canadiens
2 +# Fabri-Ka — Agrégateur de produits québécois
3 3 # Auteur : Simon-Pierre Boucher — contact@spboucher.ai
4 4 # report.py : génération du « Rapport de marché Fabri-Ka » en PDF (reportlab).
5 −# Portrait complet du commerce en ligne canadien : couverture de marque,
5 +# Portrait complet du commerce en ligne québécois : couverture de marque,
6 6 # KPIs, prix, catégories, régions, plateformes, origine, top boutiques.
7 7 # -----------------------------------------------------------------------------
8 8 from __future__ import annotations
@@ -30,8 +30,8 @@ MUTE = colors.HexColor("#6B6257")
30 30
31 31 ORIGIN_COLORS = {"A": PINE, "B": TERRA, "C": colors.HexColor("#8A6D3B"),
32 32 "D": colors.HexColor("#5B5B8A"), "E": colors.HexColor("#999999")}
33 −ORIGIN_LABELS = {"A": "Fabriqué au Canada", "B": "Conçu au Canada",
34 − "C": "Détaillant canadien", "D": "Mixte", "E": "À vérifier"}
33 +ORIGIN_LABELS = {"A": "Fabriqué au Québec", "B": "Conçu au Québec",
34 + "C": "Détaillant québécois", "D": "Mixte", "E": "À vérifier"}
35 35
36 36
37 37 def _money(v):
@@ -182,7 +182,7 @@ def _cover_page(canvas, stats):
182 182 canvas.setFillColor(INK)
183 183 canvas.setFont("Helvetica-Bold", 40)
184 184 canvas.drawString(2.2 * cm, H - 9.3 * cm, "Le commerce")
185 − canvas.drawString(2.2 * cm, H - 10.9 * cm, "en ligne canadien")
185 + canvas.drawString(2.2 * cm, H - 10.9 * cm, "en ligne québécois")
186 186 canvas.setFillColor(MUTE)
187 187 canvas.setFont("Helvetica", 13)
188 188 canvas.drawString(2.2 * cm, H - 12 * cm,
@@ -262,7 +262,7 @@ def build_report(stats: dict) -> bytes:
262 262 story.append(_h("APERÇU DU MARCHÉ", eyebrow))
263 263 story.append(_h("Les chiffres clés", h2))
264 264 story.append(_h(
265 − "Fabri-Ka recense les boutiques en ligne du Canada et agrège leurs "
265 + "Fabri-Ka recense les boutiques en ligne du Québec et agrège leurs "
266 266 "catalogues publics (Shopify, WooCommerce, Wix, Squarespace), "
267 267 "resynchronisés aux 6 heures. Ce rapport présente l'état du marché : "
268 268 "volume de produits, boutiques actives, structure des prix, répartition "
@@ -392,7 +392,7 @@ def build_report(stats: dict) -> bytes:
392 392 "aux marchands sources. La classification d'origine A–E s'appuie sur les "
393 393 "certifications (Aliments du Québec, Les Produits du Québec), l'adhésion "
394 394 "à des associations de producteurs et les mentions des sites — jamais sur "
395 − "la seule présomption qu'une entreprise est canadienne. "
395 + "la seule présomption qu'une entreprise est québécoise. "
396 396 f"Couverture : {_int(cov.get('with_image'))} produits avec image, "
397 397 f"{_int(cov.get('with_logo'))} boutiques avec logo, "
398 398 f"{_int(cov.get('with_region'))} boutiques géolocalisées.", body))
modified fabrika/schema.py +2 −12
@@ -1,5 +1,5 @@
1 1 # -----------------------------------------------------------------------------
2 −# Fabri-Ka — Agrégateur de produits canadiens (marketplace de découverte)
2 +# Fabri-Ka — Agrégateur de produits québécois (marketplace de découverte)
3 3 # Auteur : Simon-Pierre Boucher — contact@spboucher.ai
4 4 # schema.py : modèles standardisés Product / Store + normalisation
5 5 # -----------------------------------------------------------------------------
@@ -14,9 +14,7 @@ import re
14 14 import unicodedata
15 15 from dataclasses import dataclass, field, asdict
16 16
17 −# Régions Fabri-Ka : le Québec garde ses 17 régions administratives (granularité
18 −# historique du registre) ; le reste du Canada est couvert à la province/territoire.
19 −QC_REGIONS = [
17 +REGIONS = [
20 18 "Bas-Saint-Laurent", "Saguenay–Lac-Saint-Jean", "Capitale-Nationale",
21 19 "Mauricie", "Estrie", "Montréal", "Outaouais", "Abitibi-Témiscamingue",
22 20 "Côte-Nord", "Nord-du-Québec", "Gaspésie–Îles-de-la-Madeleine",
@@ -24,14 +22,6 @@ QC_REGIONS = [
24 22 "Montérégie", "Centre-du-Québec",
25 23 ]
26 24
27 −CA_PROVINCES = [
28 − "Ontario", "Colombie-Britannique", "Alberta", "Manitoba", "Saskatchewan",
29 − "Nouvelle-Écosse", "Nouveau-Brunswick", "Terre-Neuve-et-Labrador",
30 − "Île-du-Prince-Édouard", "Yukon", "Territoires du Nord-Ouest", "Nunavut",
31 −]
32 −
33 −REGIONS = QC_REGIONS + CA_PROVINCES
34 −
35 25 # Taxonomie produit Fabri-Ka (clé canonique -> libellé FR + mots-clés de match)
36 26 CATEGORIES: dict[str, tuple[str, list[str]]] = {
37 27 "erable": ("Érable & miel", ["erable", "maple", "sirop", "miel", "honey", "hydromel"]),
modified fabrika/seo.py +31 −31
@@ -1,5 +1,5 @@
1 1 # -----------------------------------------------------------------------------
2 −# Fabri-Ka — Agrégateur de produits canadiens
2 +# Fabri-Ka — Agrégateur de produits québécois
3 3 # Auteur : Simon-Pierre Boucher — contact@spboucher.ai
4 4 # seo.py : rendu serveur pour l'indexation — chaque route livre son contenu
5 5 # essentiel en HTML (title/meta/canonical/og/JSON-LD + corps de page),
@@ -26,14 +26,14 @@ from .schema import CATEGORIES, REGIONS
26 26
27 27 BASE = os.environ.get("FABRIKA_BASE_URL", "https://www.fabri-ka.com").rstrip("/")
28 28 SITE = "Fabri-Ka"
29 −DEFAULT_TITLE = ("Fabri-Ka — Tous les produits canadiens. Un seul endroit."
29 +DEFAULT_TITLE = ("Fabri-Ka — Tous les produits québécois. Un seul endroit."
30 30 " · Un service Groupe KA")
31 31 FRONT_DIST = Path(__file__).resolve().parent.parent / "frontend" / "dist"
32 32 PER_PAGE = 24
33 33 SITEMAP_CHUNK = 40_000
34 34
35 −ORIGIN_LABELS = {"A": "Fabriqué au Canada", "B": "Conçu au Canada",
36 − "C": "Détaillant canadien", "D": "Mixte", "E": "À vérifier"}
35 +ORIGIN_LABELS = {"A": "Fabriqué au Québec", "B": "Conçu au Québec",
36 + "C": "Détaillant québécois", "D": "Mixte", "E": "À vérifier"}
37 37
38 38
39 39 # ---- slugs -------------------------------------------------------------------
@@ -330,12 +330,12 @@ def page_home() -> HTMLResponse:
330 330
331 331 totals, cats, regions, top_stores, newest = cached("home", 600, _data)
332 332 desc = (f"Fabri-Ka agrège {fmt_int(totals['products'])} produits de "
333 − f"{fmt_int(totals['stores'])} boutiques en ligne canadiennes, dans les "
334 − f"{totals['regions']} régions du Canada. Produits fabriqués, conçus ou "
335 − f"vendus au Canada, tous au même endroit.")
333 + f"{fmt_int(totals['stores'])} boutiques en ligne québécoises, dans les "
334 + f"{totals['regions']} régions du Québec. Produits fabriqués, conçus ou "
335 + f"vendus au Québec, tous au même endroit.")
336 336 body = f"""
337 337 <div class="seo-page">
338 −<h1>Tous les produits canadiens. Un seul endroit.</h1>
338 +<h1>Tous les produits québécois. Un seul endroit.</h1>
339 339 <p>{esc(desc)}</p>
340 340 <p><a href="/produits">Voir les {fmt_int(totals["products"])} produits</a> ·
341 341 <a href="/boutiques">{fmt_int(totals["stores"])} boutiques</a> ·
@@ -399,21 +399,21 @@ def page_listing(category: str | None, region: str | None, params) -> Response:
399 399 h1 = f"{label} — {region}"
400 400 title = f"{label} — {region} | {SITE}"
401 401 desc = (f"{fmt_int(total)} produits « {label} » offerts par des boutiques de la région "
402 − f"{region}. Comparez les produits canadiens sur Fabri-Ka.")
402 + f"{region}. Comparez les produits québécois sur Fabri-Ka.")
403 403 elif category:
404 − h1 = f"{label} du Canada"
405 − title = f"{label} du Canada — {fmt_int(total)} produits | {SITE}"
406 − desc = (f"{fmt_int(total)} produits « {label} » de boutiques en ligne canadiennes, "
404 + h1 = f"{label} du Québec"
405 + title = f"{label} du Québec — {fmt_int(total)} produits | {SITE}"
406 + desc = (f"{fmt_int(total)} produits « {label} » de boutiques en ligne québécoises, "
407 407 f"réunis sur Fabri-Ka. Prix, disponibilité et boutiques d'origine.")
408 408 elif region:
409 − h1 = f"Produits canadiens — {region}"
409 + h1 = f"Produits québécois — {region}"
410 410 title = f"Produits de la région {region} — {fmt_int(total)} produits | {SITE}"
411 411 desc = (f"{fmt_int(total)} produits offerts par les boutiques en ligne de la région "
412 412 f"{region}, réunis sur Fabri-Ka.")
413 413 else:
414 414 h1 = "Produits"
415 − title = f"Tous les produits canadiens — {fmt_int(total)} produits | {SITE}"
416 − desc = (f"Parcourez {fmt_int(total)} produits de boutiques en ligne canadiennes : "
415 + title = f"Tous les produits québécois — {fmt_int(total)} produits | {SITE}"
416 + desc = (f"Parcourez {fmt_int(total)} produits de boutiques en ligne québécoises : "
417 417 f"alimentation, mode, maison, art et plus. Filtrez par catégorie, région et prix.")
418 418
419 419 st = listing_stats(category, region)
@@ -514,7 +514,7 @@ def page_product(slug: str | None, uid: str, params) -> Response:
514 514 <a href="/produits">Tous les produits</a></p>
515 515 </div>"""
516 516 return render(title=f"Produit retiré — {SITE}",
517 − description="Ce produit n'est plus offert. Découvrez des produits canadiens similaires.",
517 + description="Ce produit n'est plus offert. Découvrez des produits québécois similaires.",
518 518 body=body, robots="noindex", status=410)
519 519
520 520 # URL canonique : /produits/<slug>-<uid> — 301 depuis toute variante
@@ -533,7 +533,7 @@ def page_product(slug: str | None, uid: str, params) -> Response:
533 533 images = json.loads(p.get("images") or "[]")
534 534 title_txt = p["title"] or "Produit"
535 535 price_txt = fmt_price(p.get("price"))
536 − store_loc = ", ".join(x for x in [p.get("store_city"), p.get("store_region")] if x) or "Canada"
536 + store_loc = ", ".join(x for x in [p.get("store_city"), p.get("store_region")] if x) or "Québec"
537 537 desc_meta = clean_text(p.get("description") or "", 150)
538 538 meta_desc = " — ".join(x for x in [
539 539 title_txt if not desc_meta else desc_meta,
@@ -592,7 +592,7 @@ def page_product(slug: str | None, uid: str, params) -> Response:
592 592 offer = {"@type": "Offer", "url": BASE + canonical,
593 593 "price": f"{p['price']:.2f}", "priceCurrency": p.get("currency") or "CAD",
594 594 "seller": {"@type": "Organization", "name": p["store_name"]},
595 − "areaServed": {"@type": "AdministrativeArea", "name": "Canada"}}
595 + "areaServed": {"@type": "AdministrativeArea", "name": "Québec, Canada"}}
596 596 if availability:
597 597 offer["availability"] = availability
598 598 ld_product["offers"] = offer
@@ -619,21 +619,21 @@ def page_stores_index() -> HTMLResponse:
619 619 total = len(stores)
620 620 by_region: dict[str, list[dict]] = {}
621 621 for s in stores:
622 − by_region.setdefault(s["region"] or "Ailleurs au Canada", []).append(s)
622 + by_region.setdefault(s["region"] or "Ailleurs au Québec", []).append(s)
623 623 sections = "".join(
624 624 f"<h2>{esc(region)} <small>({fmt_int(len(items))} boutiques)</small></h2>"
625 625 + link_list([(f"/boutiques/{s['id']}", s["name"], f"{fmt_int(s['n'])} produits")
626 626 for s in items])
627 627 for region, items in sorted(by_region.items(), key=lambda kv: -len(kv[1])))
628 − desc = (f"Annuaire de {fmt_int(total)} boutiques en ligne canadiennes agrégées par "
628 + desc = (f"Annuaire de {fmt_int(total)} boutiques en ligne québécoises agrégées par "
629 629 f"Fabri-Ka, classées par région : alimentation, mode, maison, art et plus.")
630 630 body = f"""
631 631 <div class="seo-page">
632 −<h1>Boutiques canadiennes en ligne</h1>
632 +<h1>Boutiques québécoises en ligne</h1>
633 633 <p class="seo-stats"><b>{fmt_int(total)}</b> boutiques avec produits, classées par région.</p>
634 634 {sections}
635 635 </div>"""
636 − return render(title=f"Boutiques canadiennes — annuaire de {fmt_int(total)} boutiques en ligne | {SITE}",
636 + return render(title=f"Boutiques québécoises — annuaire de {fmt_int(total)} boutiques en ligne | {SITE}",
637 637 description=desc, canonical="/boutiques", body=body,
638 638 jsonld=[breadcrumb_ld([("Accueil", "/"), ("Boutiques", "/boutiques")])])
639 639
@@ -660,7 +660,7 @@ def page_store(store_id: str) -> Response:
660 660 finally:
661 661 con.close()
662 662
663 − loc = ", ".join(x for x in [s.get("city"), s.get("region")] if x) or "Canada"
663 + loc = ", ".join(x for x in [s.get("city"), s.get("region")] if x) or "Québec"
664 664 n = s.get("product_count") or 0
665 665 origin = ORIGIN_LABELS.get(s.get("origin_class") or "", "")
666 666 desc = clean_text(s.get("description_meta") or "", 100)
@@ -695,7 +695,7 @@ def page_store(store_id: str) -> Response:
695 695 if s.get("logo_url"):
696 696 ld["logo"] = s["logo_url"]
697 697 crumbs = [("Accueil", "/"), ("Boutiques", "/boutiques"), (s["name"], f"/boutiques/{store_id}")]
698 − return render(title=f"{s['name']} — {fmt_int(n)} produits canadiens | {SITE}",
698 + return render(title=f"{s['name']} — {fmt_int(n)} produits québécois | {SITE}",
699 699 description=meta_desc, canonical=f"/boutiques/{store_id}", body=body,
700 700 jsonld=[ld, breadcrumb_ld(crumbs)],
701 701 og={"og:image": s.get("cover_url") or s.get("logo_url") or ""})
@@ -712,23 +712,23 @@ def page_stats() -> HTMLResponse:
712 712 finally:
713 713 con.close()
714 714 t = cached("stats_head", 600, _data)
715 − desc = (f"Statistiques du marché des produits canadiens en ligne : "
715 + desc = (f"Statistiques du marché des produits québécois en ligne : "
716 716 f"{fmt_int(t['products'])} produits, {fmt_int(t['stores'])} boutiques — "
717 717 f"prix par catégorie, répartition régionale, croissance du catalogue.")
718 718 body = f"""
719 719 <div class="seo-page">
720 −<h1>Statistiques — produits canadiens en ligne</h1>
720 +<h1>Statistiques — produits québécois en ligne</h1>
721 721 <p>{esc(desc)}</p>
722 722 <p><a href="/api/report.pdf">Télécharger le rapport de marché (PDF)</a> ·
723 723 <a href="/produits">Parcourir les produits</a></p>
724 724 </div>"""
725 − return render(title=f"Statistiques du marché canadien en ligne | {SITE}",
725 + return render(title=f"Statistiques du marché québécois en ligne | {SITE}",
726 726 description=desc, canonical="/stats", body=body)
727 727
728 728
729 729 def page_about() -> HTMLResponse:
730 730 desc = ("Fabri-Ka est un agrégateur indépendant qui réunit les catalogues publics des "
731 − "boutiques en ligne canadiennes : produits fabriqués, conçus ou vendus au Canada.")
731 + "boutiques en ligne québécoises : produits fabriqués, conçus ou vendus au Québec.")
732 732 body = f"""
733 733 <div class="seo-page">
734 734 <h1>À propos de Fabri-Ka</h1>
@@ -741,7 +741,7 @@ vers la boutique d'origine pour l'achat. Contact : contact@spboucher.ai</p>
741 741
742 742
743 743 def page_inscrire() -> HTMLResponse:
744 − desc = ("Artisans et fabricants canadiens : inscrivez votre atelier sur Fabri-Ka. "
744 + desc = ("Artisans et fabricants québécois : inscrivez votre atelier sur Fabri-Ka. "
745 745 "Chaque fiche est validée manuellement avant publication.")
746 746 body = f"""
747 747 <div class="seo-page">
@@ -749,7 +749,7 @@ def page_inscrire() -> HTMLResponse:
749 749 <p>{esc(desc)}</p>
750 750 <p>Remplissez le formulaire (nom, métier, description, coordonnées, site web) —
751 751 votre demande part en file de validation ; rien n'est publié sans vérification
752 −de la fabrication canadienne et des coordonnées.</p>
752 +de la fabrication québécoise et des coordonnées.</p>
753 753 <p><a href="/produits">Produits</a> · <a href="/boutiques">Boutiques</a></p>
754 754 </div>"""
755 755 return render(title=f"Inscrire mon atelier | {SITE}", description=desc,
@@ -804,7 +804,7 @@ def page_404() -> HTMLResponse:
804 804 <a href="/boutiques">Boutiques</a></p>
805 805 </div>"""
806 806 return render(title=f"Page introuvable — {SITE}",
807 − description="Cette page n'existe pas. Découvrez les produits canadiens sur Fabri-Ka.",
807 + description="Cette page n'existe pas. Découvrez les produits québécois sur Fabri-Ka.",
808 808 body=body, robots="noindex", status=404)
809 809
810 810
modified fabrika/statsdash.py +3 −3
@@ -1,5 +1,5 @@
1 1 # -----------------------------------------------------------------------------
2 −# Fabri-Ka — Agrégateur de produits canadiens
2 +# Fabri-Ka — Agrégateur de produits québécois
3 3 # Auteur : Simon-Pierre Boucher — contact@spboucher.ai
4 4 # statsdash.py : tableau de bord /api/stats/dashboard (contrat ka-stats SPEC v2)
5 5 # -----------------------------------------------------------------------------
@@ -37,8 +37,8 @@ PLATFORM_LABELS = {
37 37 }
38 38
39 39 ORIGIN_LABELS = {
40 − "A": "A — Fabriqué au Canada", "B": "B — Conçu au Canada",
41 − "C": "C — Détaillant canadien", "D": "D — Mixte", "E": "E — À vérifier",
40 + "A": "A — Fabriqué au Québec", "B": "B — Conçu au Québec",
41 + "C": "C — Détaillant québécois", "D": "D — Mixte", "E": "E — À vérifier",
42 42 }
43 43
44 44 BUCKETS = [
modified fabrika/web.py +3 −3
@@ -1,5 +1,5 @@
1 1 # -----------------------------------------------------------------------------
2 −# Fabri-Ka — Agrégateur de produits canadiens
2 +# Fabri-Ka — Agrégateur de produits québécois
3 3 # Auteur : Simon-Pierre Boucher — contact@spboucher.ai
4 4 # web.py : API FastAPI + service du frontend React (dist/)
5 5 # -----------------------------------------------------------------------------
@@ -452,7 +452,7 @@ def stats_report(period: str = "30j",
452 452 raise HTTPException(400, "Dates invalides (format attendu YYYY-MM-DD)")
453 453 site = {"wordmark": "Fabri·Ka", "accent": "#c4532e",
454 454 "domain": "www.fabri-ka.com",
455 − "tagline": "Tous les produits canadiens. Un seul endroit."}
455 + "tagline": "Tous les produits québécois. Un seul endroit."}
456 456 if mode not in kapdf.REPORT_MODES:
457 457 mode = "complet"
458 458 pdf = kapdf.GroupeKAReport(
@@ -492,7 +492,7 @@ def stats_report_custom(spec: dict = Body(...)):
492 492 raise HTTPException(400, "Dates invalides (format attendu YYYY-MM-DD)")
493 493 site = {"wordmark": "Fabri·Ka", "accent": "#c4532e",
494 494 "domain": "www.fabri-ka.com",
495 − "tagline": "Tous les produits canadiens. Un seul endroit."}
495 + "tagline": "Tous les produits québécois. Un seul endroit."}
496 496 known = {b["key"] for b in kapdf.catalog(dash)}
497 497 blocks = [b for b in (spec.get("blocks") or [])
498 498 if isinstance(b, dict) and b.get("key") in known][:40]
modified frontend/index.html +1 −1
@@ -11,7 +11,7 @@
11 11 <meta name="theme-color" content="#C4532E" />
12 12 <meta
13 13 name="description"
14 − content="Fabri-Ka agrège les produits des boutiques en ligne canadiennes. Trouvez des produits fabriqués, conçus ou vendus au Canada, tous au même endroit."
14 + content="Fabri-Ka agrège les produits des boutiques en ligne québécoises. Trouvez des produits fabriqués, conçus ou vendus au Québec, tous au même endroit."
15 15 />
16 16 <link rel="manifest" href="/manifest.json" />
17 17 <link rel="preconnect" href="https://fonts.googleapis.com" />
modified frontend/public/manifest.json +1 −1
@@ -1,7 +1,7 @@
1 1 {
2 2 "name": "Fabri-Ka",
3 3 "short_name": "Fabri-Ka",
4 − "description": "Tous les produits canadiens. Un seul endroit.",
4 + "description": "Tous les produits québécois. Un seul endroit.",
5 5 "start_url": "/",
6 6 "display": "standalone",
7 7 "background_color": "#FAF6F0",
modified frontend/src/api.ts +4 −22
@@ -477,24 +477,6 @@ export const REGIONS_QC = [
477 477 'Centre-du-Québec',
478 478 ]
479 479
480 −// Hors-Québec : granularité province/territoire (portée Canada)
481 −export const REGIONS_CA = [
482 − 'Ontario',
483 − 'Colombie-Britannique',
484 − 'Alberta',
485 − 'Manitoba',
486 − 'Saskatchewan',
487 − 'Nouvelle-Écosse',
488 − 'Nouveau-Brunswick',
489 − 'Terre-Neuve-et-Labrador',
490 − 'Île-du-Prince-Édouard',
491 − 'Yukon',
492 − 'Territoires du Nord-Ouest',
493 − 'Nunavut',
494 −]
495 −
496 −export const REGIONS_ALL = [...REGIONS_QC, ...REGIONS_CA]
497 −
498 480 export const CATEGORY_LABELS: Record<string, string> = {
499 481 erable: 'Érable & miel',
500 482 epicerie: 'Épicerie fine',
@@ -546,7 +528,7 @@ export function regionSlug(region: string): string {
546 528 }
547 529
548 530 export function regionFromSlug(slug: string): string | null {
549 − return REGIONS_ALL.find((r) => slugify(r) === slug) ?? null
531 + return REGIONS_QC.find((r) => slugify(r) === slug) ?? null
550 532 }
551 533
552 534 /** Met à jour title / meta description / canonical lors des navigations SPA. */
@@ -569,9 +551,9 @@ export function setPageMeta(title: string, description?: string): void {
569 551 export type OriginClass = 'A' | 'B' | 'C' | 'D' | 'E'
570 552
571 553 export const ORIGIN_LABELS: Record<OriginClass, string> = {
572 − A: 'Fabriqué au Canada',
573 − B: 'Conçu au Canada',
574 − C: 'Détaillant canadien',
554 + A: 'Fabriqué au Québec',
555 + B: 'Conçu au Québec',
556 + C: 'Détaillant québécois',
575 557 D: 'Mixte',
576 558 E: 'À vérifier',
577 559 }
modified frontend/src/components/BottomNav.tsx +2 −2
@@ -62,7 +62,7 @@ export default function BottomNav() {
62 62 type="search"
63 63 value={query}
64 64 onChange={(e) => setQuery(e.target.value)}
65 − placeholder="Rechercher un produit canadien…"
65 + placeholder="Rechercher un produit québécois…"
66 66 aria-label="Rechercher"
67 67 enterKeyHint="search"
68 68 />
@@ -77,7 +77,7 @@ export default function BottomNav() {
77 77 </button>
78 78 </div>
79 79 <p className="search-overlay-hint">
80 − Produits fabriqués, conçus et vendus au Canada.
80 + Produits fabriqués, conçus et vendus au Québec.
81 81 </p>
82 82 </div>
83 83 )}
modified frontend/src/components/SearchBar.tsx +1 −1
@@ -14,7 +14,7 @@ interface Props {
14 14
15 15 export default function SearchBar({
16 16 compact = false,
17 − placeholder = 'Rechercher un produit canadien…',
17 + placeholder = 'Rechercher un produit québécois…',
18 18 initialValue = '',
19 19 inputId,
20 20 shortcutHint = false,
modified frontend/src/components/StoreCard.tsx +1 −1
@@ -22,7 +22,7 @@ export default function StoreCard({ store }: { store: Store }) {
22 22 </div>
23 23 <p className="store-card-location">
24 24 <IconMapPin size={14} />
25 − {[store.city, store.region].filter(Boolean).join(', ') || 'Canada'}
25 + {[store.city, store.region].filter(Boolean).join(', ') || 'Québec'}
26 26 </p>
27 27 <div className="store-card-meta">
28 28 {store.platform && (
modified frontend/src/ka/ecosystem.json +1 −1
@@ -112,7 +112,7 @@
112 112 "accentSoft": "#f7e3da",
113 113 "accentDeep": "#a94525",
114 114 "onAccent": "#ffffff",
115 − "tagline": "Les produits fabriqués au Canada"
115 + "tagline": "Les produits fabriqués au Québec"
116 116 },
117 117 {
118 118 "id": "food-ka",
modified frontend/src/pages/About.tsx +7 −7
@@ -2,10 +2,10 @@ import { ORIGIN_KEYS, ORIGIN_LABELS } from '../api'
2 2 import OriginBadge from '../components/OriginBadge'
3 3
4 4 const ORIGIN_DESCRIPTIONS: Record<string, string> = {
5 − A: 'Produits fabriqués au Canada, par des artisans ou des entreprises d’ici.',
6 − B: 'Produits conçus au Canada, dont la fabrication peut se faire ailleurs.',
7 − C: 'Boutique canadienne qui vend des produits d’origines diverses.',
8 − D: 'Catalogue mixte : une partie des produits est fabriquée ou conçue au Canada.',
5 + A: 'Produits fabriqués au Québec, par des artisans ou des entreprises d’ici.',
6 + B: 'Produits conçus au Québec, dont la fabrication peut se faire ailleurs.',
7 + C: 'Boutique québécoise qui vend des produits d’origines diverses.',
8 + D: 'Catalogue mixte : une partie des produits est fabriquée ou conçue au Québec.',
9 9 E: 'Origine en cours de vérification par notre équipe.',
10 10 }
11 11
@@ -17,9 +17,9 @@ export default function About() {
17 17 <div className="about-content">
18 18 <p>
19 19 <strong>Fabri-Ka</strong> agrège les catalogues publics des boutiques
20 − en ligne canadiennes pour offrir un seul point d'entrée vers les
20 + en ligne québécoises pour offrir un seul point d'entrée vers les
21 21 produits d'ici. Pensez-y comme un moteur de recherche dédié aux
22 − produits canadiens : on rassemble, on classe, on met en valeur.
22 + produits québécois : on rassemble, on classe, on met en valeur.
23 23 </p>
24 24 <p>
25 25 Chaque produit affiché sur Fabri-Ka <strong>renvoie directement vers
@@ -48,7 +48,7 @@ export default function About() {
48 48
49 49 <h2>Contact</h2>
50 50 <p>
51 − Vous êtes une boutique canadienne et souhaitez être ajoutée (ou
51 + Vous êtes une boutique québécoise et souhaitez être ajoutée (ou
52 52 retirée) ? Une erreur de classification ? Écrivez-nous :{' '}
53 53 <a href="mailto:contact@spboucher.ai">contact@spboucher.ai</a>
54 54 </p>
modified frontend/src/pages/Catalog.tsx +3 −3
@@ -188,15 +188,15 @@ export default function Catalog() {
188 188 fixedCategory && fixedRegion
189 189 ? `${catLabel} — ${fixedRegion}`
190 190 : fixedCategory
191 − ? `${catLabel} du Canada`
191 + ? `${catLabel} du Québec`
192 192 : fixedRegion
193 − ? `Produits canadiens — ${fixedRegion}`
193 + ? `Produits québécois — ${fixedRegion}`
194 194 : 'Produits'
195 195
196 196 useEffect(() => {
197 197 setPageMeta(
198 198 heading === 'Produits'
199 − ? 'Tous les produits canadiens | Fabri-Ka'
199 + ? 'Tous les produits québécois | Fabri-Ka'
200 200 : `${heading} | Fabri-Ka`
201 201 )
202 202 }, [heading])
modified frontend/src/pages/Home.tsx +3 −3
@@ -40,7 +40,7 @@ export default function Home() {
40 40 const [facets, setFacets] = useState<Facets | null>(null)
41 41
42 42 useEffect(() => {
43 − setPageMeta('Fabri-Ka — Tous les produits canadiens. Un seul endroit.')
43 + setPageMeta('Fabri-Ka — Tous les produits québécois. Un seul endroit.')
44 44 }, [])
45 45
46 46 useEffect(() => {
@@ -87,13 +87,13 @@ export default function Home() {
87 87 <section className="hero">
88 88 <p className="hero-eyebrow">Le répertoire des produits d'ici</p>
89 89 <h1 className="hero-title">
90 − Tous les produits canadiens.
90 + Tous les produits québécois.
91 91 <br />
92 92 <em>Un seul endroit.</em>
93 93 </h1>
94 94 <p className="hero-sub">
95 95 Fabri-Ka rassemble les catalogues des boutiques en ligne d'ici —
96 − fabriqué, conçu et vendu au Canada.
96 + fabriqué, conçu et vendu au Québec.
97 97 </p>
98 98
99 99 <div className="hero-search">
modified frontend/src/pages/Inscrire.tsx +7 −7
@@ -33,7 +33,7 @@ export default function Inscrire() {
33 33 useEffect(() => {
34 34 setPageMeta(
35 35 'Inscrire mon atelier — Fabri-Ka',
36 − 'Artisans et fabricants canadiens : inscrivez votre atelier sur ' +
36 + 'Artisans et fabricants québécois : inscrivez votre atelier sur ' +
37 37 'Fabri-Ka. Chaque fiche est validée manuellement avant publication.'
38 38 )
39 39 fetchFacets()
@@ -81,7 +81,7 @@ export default function Inscrire() {
81 81 <h1>Demande reçue !</h1>
82 82 <p className="contact-lede">{done}</p>
83 83 <p className="contact-note">
84 − Notre équipe vérifie chaque atelier (fabrication canadienne,
84 + Notre équipe vérifie chaque atelier (fabrication québécoise,
85 85 coordonnées, site) avant de publier la fiche. Vous serez joint aux
86 86 coordonnées fournies si un détail manque.
87 87 </p>
@@ -91,11 +91,11 @@ export default function Inscrire() {
91 91
92 92 return (
93 93 <div className="page inscrire-page">
94 − <span className="kicker">Artisans &amp; fabricants canadiens</span>
94 + <span className="kicker">Artisans &amp; fabricants québécois</span>
95 95 <h1>Inscrire mon atelier</h1>
96 96 <p className="contact-lede">
97 − Vous fabriquez au Canada ? Inscrivez votre atelier : après une{' '}
98 − <b>validation manuelle</b> (fabrication canadienne et coordonnées
97 + Vous fabriquez au Québec ? Inscrivez votre atelier : après une{' '}
98 + <b>validation manuelle</b> (fabrication québécoise et coordonnées
99 99 vérifiables), votre fiche apparaîtra dans le répertoire — et si votre
100 100 boutique en ligne expose un catalogue public, vos produits seront
101 101 ajoutés automatiquement.
@@ -150,7 +150,7 @@ export default function Inscrire() {
150 150 maxLength={300}
151 151 value={form.address}
152 152 onChange={(e) => set('address', e.target.value)}
153 − placeholder="123 rue Principale, Ville (Province) A0A 0A0"
153 + placeholder="123 rue Principale, Ville (Québec) G0X 0X0"
154 154 />
155 155 </label>
156 156 <label style={FIELD}>
@@ -239,7 +239,7 @@ export default function Inscrire() {
239 239 </button>
240 240 <p className="contact-note">
241 241 En soumettant, vous confirmez que les produits sont fabriqués (ou
242 − conçus) au Canada. Un courriel ou un téléphone est requis pour la
242 + conçus) au Québec. Un courriel ou un téléphone est requis pour la
243 243 vérification. Aucune fiche n&rsquo;est publiée sans validation
244 244 manuelle.
245 245 </p>
modified frontend/src/pages/NotFound.tsx +1 −1
@@ -6,7 +6,7 @@ export default function NotFound() {
6 6 <div className="page">
7 7 <div className="notfound">
8 8 <h1>404</h1>
9 − <EmptyState message="Cette page n'existe pas — mais nos produits canadiens, oui.">
9 + <EmptyState message="Cette page n'existe pas — mais nos produits québécois, oui.">
10 10 <Link className="btn btn-primary" to="/">
11 11 Retour à l'accueil
12 12 </Link>
modified frontend/src/pages/ProductDetail.tsx +1 −1
@@ -582,7 +582,7 @@ export default function ProductDetail() {
582 582 <p className="store-info-location">
583 583 {[product.store_city, product.store_region]
584 584 .filter(Boolean)
585 − .join(', ') || 'Canada'}
585 + .join(', ') || 'Québec'}
586 586 </p>
587 587 </div>
588 588 </div>
modified frontend/src/pages/Profile.tsx +1 −1
@@ -1,5 +1,5 @@
1 1 // -----------------------------------------------------------------------------
2 −// Fabri-Ka — Agrégateur de produits canadiens
2 +// Fabri-Ka — Agrégateur de produits québécois
3 3 // Auteur : Simon-Pierre Boucher — contact@spboucher.ai
4 4 // pages/Profile.tsx : profil utilisateur — carte de membre Groupe KA (KA-ID),
5 5 // profil Groupe KA en lecture seule (bio, emploi, ville, âge, site, réseaux
modified frontend/src/pages/Stats.tsx +2 −2
@@ -73,7 +73,7 @@ export default function Stats() {
73 73 useEffect(() => {
74 74 document.title = 'Statistiques — Fabri-Ka'
75 75 return () => {
76 − document.title = 'Fabri-Ka — Tous les produits canadiens. Un seul endroit.'
76 + document.title = 'Fabri-Ka — Tous les produits québécois. Un seul endroit.'
77 77 }
78 78 }, [])
79 79
@@ -121,7 +121,7 @@ export default function Stats() {
121 121 <p className="klabel" style={{ margin: 0 }}>Fabri·Ka · Groupe KA</p>
122 122 <h1 style={{ margin: '4px 0 0', fontSize: 'clamp(26px, 4vw, 38px)' }}>Statistiques</h1>
123 123 <p style={{ margin: '6px 0 0', color: 'var(--ink-2)', fontSize: 14 }}>
124 − Le catalogue des produits canadiens, mesuré en continu — volumes, prix,
124 + Le catalogue des produits québécois, mesuré en continu — volumes, prix,
125 125 catégories, régions et boutiques.
126 126 </p>
127 127 </div>
modified frontend/src/pages/StoreDetail.tsx +4 −4
@@ -34,7 +34,7 @@ import Skeleton, { SkeletonGrid } from '../components/Skeleton'
34 34 import StoreLogo from '../components/StoreLogo'
35 35
36 36 const PER_PAGE = 24
37 −const DEFAULT_TITLE = 'Fabri-Ka — Tous les produits canadiens. Un seul endroit.'
37 +const DEFAULT_TITLE = 'Fabri-Ka — Tous les produits québécois. Un seul endroit.'
38 38
39 39 const SORT_OPTIONS: { value: string; label: string }[] = [
40 40 { value: 'recent', label: 'Plus récents' },
@@ -223,7 +223,7 @@ export default function StoreDetail() {
223 223 })
224 224 const stats = store?.product_stats
225 225 const location = store
226 − ? [store.city, store.region].filter(Boolean).join(', ') || 'Canada'
226 + ? [store.city, store.region].filter(Boolean).join(', ') || 'Québec'
227 227 : ''
228 228 const lastSyncRel = formatRelativeDate(store?.last_sync ?? null)
229 229 const priceRange =
@@ -412,7 +412,7 @@ export default function StoreDetail() {
412 412 )}
413 413 <div className="store-stat-tile">
414 414 <span className="store-stat-value store-stat-value-sm">
415 − {store.region || 'Canada'}
415 + {store.region || 'Québec'}
416 416 </span>
417 417 <span className="store-stat-label">
418 418 {store.platform ? `Région · ${store.platform}` : 'Région'}
@@ -545,7 +545,7 @@ export default function StoreDetail() {
545 545 <span className="store-mini-body">
546 546 <span className="store-mini-name">{s.name}</span>
547 547 <span className="store-mini-meta">
548 − {s.region || 'Canada'} · {formatInt(s.product_count)} produit
548 + {s.region || 'Québec'} · {formatInt(s.product_count)} produit
549 549 {s.product_count === 1 ? '' : 's'}
550 550 </span>
551 551 </span>
modified scripts/build_registry.py +13 −37
@@ -3,7 +3,7 @@
3 3
4 4 Merges:
5 5 data/enriched/candidates.jsonl (discovery provenance)
6 − data/enriched/verified.jsonl (liveness, platform, Canada signals)
6 + data/enriched/verified.jsonl (liveness, platform, QC signals)
7 7 Applies origin classification A–E with evidence, assigns region,
8 8 and emits the canonical registry used by fabrika/ingest.py.
9 9 """
@@ -156,20 +156,7 @@ ALL_REGIONS = ["Bas-Saint-Laurent", "Saguenay–Lac-Saint-Jean", "Capitale-Natio
156 156 "Mauricie", "Estrie", "Montréal", "Outaouais", "Abitibi-Témiscamingue",
157 157 "Côte-Nord", "Nord-du-Québec", "Gaspésie–Îles-de-la-Madeleine",
158 158 "Chaudière-Appalaches", "Laval", "Lanaudière", "Laurentides",
159 − "Montérégie", "Centre-du-Québec",
160 − # hors-Québec : granularité province/territoire (portée Canada 2026-08-28)
161 − "Ontario", "Colombie-Britannique", "Alberta", "Manitoba", "Saskatchewan",
162 − "Nouvelle-Écosse", "Nouveau-Brunswick", "Terre-Neuve-et-Labrador",
163 − "Île-du-Prince-Édouard", "Yukon", "Territoires du Nord-Ouest", "Nunavut"]
164 −
165 −# première lettre du code postal -> province/territoire (hors G/H/J = Québec,
166 −# résolu plus finement par FSA_MAP)
167 −POSTAL_LETTER_PROVINCE = {
168 − "A": "Terre-Neuve-et-Labrador", "B": "Nouvelle-Écosse", "C": "Île-du-Prince-Édouard",
169 − "E": "Nouveau-Brunswick", "K": "Ontario", "L": "Ontario", "M": "Ontario",
170 − "N": "Ontario", "P": "Ontario", "R": "Manitoba", "S": "Saskatchewan",
171 − "T": "Alberta", "V": "Colombie-Britannique", "Y": "Yukon",
172 −}
159 + "Montérégie", "Centre-du-Québec"]
173 160
174 161
175 162 try:
@@ -181,16 +168,8 @@ except FileNotFoundError:
181 168
182 169 def region_from_postal(cand, ver):
183 170 for p in (cand.get("postal_prefix"), (ver.get("qc_postal") or "")[:3] or None):
184 − if not p:
185 − continue
186 − p = p.upper()
187 − if p in FSA_MAP:
188 − return FSA_MAP[p]
189 − letter = p[0]
190 − if letter == "X": # X0A/X0B/X0C = Nunavut, le reste = TNO
191 − return "Nunavut" if p in ("X0A", "X0B", "X0C") else "Territoires du Nord-Ouest"
192 − if letter in POSTAL_LETTER_PROVINCE:
193 − return POSTAL_LETTER_PROVINCE[letter]
171 + if p and p.upper() in FSA_MAP:
172 + return FSA_MAP[p.upper()]
194 173 return ""
195 174
196 175
@@ -228,15 +207,15 @@ def classify(cand, ver):
228 207 else:
229 208 cls, conf, ev = "E", 0.3, "Découvert sans certification connue"
230 209 if ver.get("made_in_qc_wording"):
231 − ev += " ; le site affiche une mention de fabrication canadienne"
210 + ev += " ; le site affiche une mention de fabrication québécoise"
232 211 if cls in ("E", "C"):
233 212 cls, conf = "A", max(conf, 0.6)
234 213 else:
235 214 conf = min(0.98, conf + 0.05)
236 − loc_signals = sum(bool(ver.get(k)) for k in ("qc_postal", "qc_phone", "tld_quebec", "mentions_quebec"))
237 − if loc_signals == 0 and cls != "E":
215 + qc_signals = sum(bool(ver.get(k)) for k in ("qc_postal", "qc_phone", "tld_quebec", "mentions_quebec"))
216 + if qc_signals == 0 and cls != "E":
238 217 conf = max(0.2, conf - 0.25)
239 − ev += " ; aucun signal de localisation canadienne détecté sur le site (à vérifier)"
218 + ev += " ; aucun signal de localisation québécoise détecté sur le site (à vérifier)"
240 219 return cls, round(conf, 2), ev
241 220
242 221
@@ -279,15 +258,12 @@ def main():
279 258 if s not in seen_final[final_dom]["discovery_sources"]:
280 259 seen_final[final_dom]["discovery_sources"].append(s)
281 260 continue
282 − # garde-fou localisation : preuve CANADA sur le site OU source de confiance.
283 − # Depuis 2026-08-28 les champs qc_* de verify.py capturent des signaux
284 − # pan-canadiens (codes postaux A–Y, indicatifs de toutes les provinces,
285 − # mentions Canada/provinces, « made in Canada »).
286 − ca_signal = any(ver.get(k) for k in ("qc_postal", "qc_phone", "tld_quebec",
261 + # garde-fou localisation : preuve QC sur le site OU source exclusivement QC
262 + qc_signal = any(ver.get(k) for k in ("qc_postal", "qc_phone", "tld_quebec",
287 263 "mentions_quebec", "made_in_qc_wording"))
288 − trusted_source = any(s in QC_ONLY_SOURCES for s in cand.get("sources", []))
289 − if not ca_signal and not trusted_source:
290 − continue # ex. liens externes hors-Canada
264 + qc_source = any(s in QC_ONLY_SOURCES for s in cand.get("sources", []))
265 + if not qc_signal and not qc_source:
266 + continue # ex. économusées hors-Québec, liens externes non-QC
291 267 cls, conf, ev = classify(cand, ver)
292 268 default_cat = next((SOURCE_PRIORS[s][3] for s in PRIORITY
293 269 if s in cand.get("sources", []) and SOURCE_PRIORS[s][3]), None)
modified scripts/verify.py +10 −36
@@ -7,8 +7,8 @@ For each domain from data/enriched/candidates.jsonl:
7 7 (Shopify /products.json, WooCommerce /wp-json/wc/store/v1/products,
8 8 Squarespace ?format=json)
9 9 - detect cart/checkout signals
10 − - detect Canada-location signals (postal codes, area codes, mentions)
11 − - detect language, social links, "made in Canada/Quebec" wording
10 + - detect Quebec-location signals (postal codes GHJ, area codes, mentions)
11 + - detect language, social links, "made in Quebec" wording
12 12 Results cached per-domain in data/verify_cache/. Output:
13 13 data/enriched/verified.jsonl
14 14 """
@@ -30,38 +30,13 @@ os.makedirs(CACHE, exist_ok=True)
30 30 HDRS = {"User-Agent": "Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/126.0.0.0 Safari/537.36 FabriKaBot/1.0 (+contact@spboucher.ai)",
31 31 "Accept-Language": "fr-CA,fr;q=0.9,en;q=0.8"}
32 32
33 −# Portée CANADA (2026-08-28) : les champs gardent leurs noms historiques qc_*
34 −# (compat cache/registry) mais les signaux couvrent tout le pays.
35 −# Codes postaux canadiens : première lettre valide A B C E G H J K L M N P R S T V X Y.
36 −POSTAL_RE = re.compile(r"\b[ABCEGHJKLMNPRSTVXY]\d[A-Z]\s?\d[A-Z]\d\b")
37 −# Indicatifs téléphoniques canadiens (toutes provinces + territoires).
38 −AREA_RE = re.compile(
39 − r"\(?\b("
40 − r"418|514|450|819|579|581|438|873|367|263|354|468" # QC
41 − r"|416|647|437|905|289|365|742|613|343|753|519|226|548|705|249|683|807|382" # ON
42 − r"|604|778|236|672|250|257" # BC
43 − r"|403|587|825|780|368" # AB
44 − r"|306|639|474" # SK
45 − r"|204|431|584" # MB
46 − r"|902|782" # NS / PE
47 − r"|506|428" # NB
48 − r"|709|879" # NL
49 − r"|867" # YT / NT / NU
50 − r")\)?[\s.\-]?\d{3}[\s.\-]?\d{4}\b")
51 −MADE_RE = re.compile(r"(?i)(fabriqu[ée]s?\s+(au|ici\s+au)\s+(qu[ée]bec|canada)|"
52 − r"fait(es)?\s+(au|ici\s+au)\s+(qu[ée]bec|canada)|"
53 − r"made\s+in\s+(qu[ée]bec|canada)|canadian[\s\-]made|"
54 − r"con[çc]u\s+(et\s+fabriqu[ée]?\s+)?au\s+(qu[ée]bec|canada)|"
55 − r"produit\s+(du\s+qu[ée]bec|du\s+canada|canadien)|"
56 − r"fait\s+[àa]\s+la\s+main\s+au\s+(qu[ée]bec|canada)|"
57 − r"artisanal|fait\s+ici|100\s?%\s?(qu[ée]b[ée]cois|canadien)|"
58 − r"proudly\s+canadian|fi[èe]rement\s+canadien)")
59 −QC_WORD_RE = re.compile(r"(?i)\bqu[ée]bec\b|\bqc\b|\bcanada\b|\bcanadian\b|\bcanadien(ne)?s?\b"
60 − r"|\bontario\b|\balberta\b|\bmanitoba\b|\bsaskatchewan\b"
61 − r"|\bbritish\s+columbia\b|\bcolombie[\s\-]britannique\b"
62 − r"|\bnova\s+scotia\b|\bnouvelle[\s\-][ée]cosse\b"
63 − r"|\bnew\s+brunswick\b|\bnouveau[\s\-]brunswick\b"
64 − r"|\bnewfoundland\b|\bterre[\s\-]neuve\b|\byukon\b|\bnunavut\b")
33 +POSTAL_RE = re.compile(r"\b[GHJ]\d[A-Z]\s?\d[A-Z]\d\b")
34 +AREA_RE = re.compile(r"\(?\b(418|514|450|819|579|581|438|873|367)\)?[\s.\-]?\d{3}[\s.\-]?\d{4}\b")
35 +MADE_RE = re.compile(r"(?i)(fabriqu[ée]s?\s+(au|ici\s+au)\s+qu[ée]bec|fait(es)?\s+(au|ici\s+au)\s+qu[ée]bec|"
36 + r"made\s+in\s+qu[ée]bec|con[çc]u\s+(et\s+fabriqu[ée]?\s+)?au\s+qu[ée]bec|"
37 + r"produit\s+du\s+qu[ée]bec|fait\s+[àa]\s+la\s+main\s+au\s+qu[ée]bec|"
38 + r"artisanal|fait\s+ici|100\s?%\s?qu[ée]b[ée]cois)")
39 +QC_WORD_RE = re.compile(r"(?i)\bqu[ée]bec\b|\bqc\b")
65 40 CART_RE = re.compile(r"(?i)(add[\s_\-]?to[\s_\-]?cart|ajouter\s+au\s+panier|/cart\b|/panier\b|"
66 41 r"checkout|caisse|mon\s+panier|shopping[\s_\-]?cart|data-product-form)")
67 42
@@ -163,8 +138,7 @@ def verify_domain(domain):
163 138 area = AREA_RE.search(text)
164 139 rec["qc_phone"] = area.group(0) if area else None
165 140 rec["mentions_quebec"] = bool(QC_WORD_RE.search(text))
166 − # .ca exige une présence canadienne (règles CIRA) -> signal de localisation valide
167 − rec["tld_quebec"] = final_dom.endswith((".quebec", ".qc.ca", ".ca"))
141 + rec["tld_quebec"] = final_dom.endswith(".quebec") or final_dom.endswith(".qc.ca")
168 142 rec["socials"] = list(dict.fromkeys(SOCIAL_RE.findall(text)))[:4]
169 143 fr_hits = len(re.findall(r"(?i)\b(nous|votre|panier|boutique|livraison|accueil|produits)\b", text))
170 144 en_hits = len(re.findall(r"(?i)\b(shop|cart|shipping|home|about us|products)\b", text))
171 145