Nouvelle source de recettes : quitoque.fr #37

Closed
opened 2026-09-05 15:46:05 +02:00 by kyuno · 0 comments
Owner

Contexte

Ajouter https://www.quitoque.fr/recettes comme nouvelle source de recettes
importable, au même titre que TheMealDB/Marmiton/750g/Manger Bouger.

Ce n'est pas un cas neuf : le repo a déjà 4 adaptateurs enregistrés
(apps/api/src/sources/) — TheMealDB (API officielle) et trois sites
français scrapés
(Marmiton, 750g, Manger Bouger), tous construits sur un
même bloc générique jsonLdRecipeAdapter (json-ld-recipe.ts) qui lit les
données structurées schema.org/Recipe que la plupart des sites de recettes
embarquent en JSON-LD pour Google. Le précédent le plus proche pour Quitoque
est Marmiton (marmiton.ts) — le plus fin des 3 wrappers scrapés :
fetchDetail/parse délégués tels quels au bloc générique, seul list()
étant spécifique au site.

(Note en passant, hors sujet de ce ticket : specs/backend-architecture.md
— section "Sources externes", ~lignes 500-600 — est obsolète, elle ne
documente que TheMealDB + le bloc générique et affirme à tort que
registerAllRecipeSources() "n'enregistre aujourd'hui que TheMealDB". Le
vrai code (apps/api/src/sources/index.ts) enregistre déjà les 4. Ne pas
mélanger ce rattrapage avec ce ticket — seule la section Quitoque doit y
être ajoutée ici.)

Vérifications effectuées sur quitoque.fr

Vérifié en direct (navigateur) plutôt que deviné :

  • robots.txt (https://www.quitoque.fr/robots.txt) : /recettes
    n'apparaît dans aucun disallow — autorisé pour user-agent: * et
    explicitement listé allow: / pour ClaudeBot/GPTBot/PerplexityBot/
    Google-Extended nommément. Aucun des 3 adaptateurs scrapés existants ne
    documente une vérification robots.txt/CGU — celle-ci va donc plus loin
    que le précédent du repo, dans le bon sens.
  • Page détail d'une recette (/recettes/<slug>) : contient un bloc
    <script type="application/ld+json"> avec un Recipe schema.org complet
    et propre (vérifié sur "Mijoté de lentilles vertes au lait de coco et
    chou romanesco") : name, description, image, prepTime/totalTime
    (ISO 8601), recipeYield: "2", recipeIngredient: ["150 g lentilles vertes (Bio)", "1 gousse d'ail", "0.5 citron jaune (Bio)", "1 à 3 cm gingembre", ...], recipeInstructions en HowToStep[] (name + text
    par étape). Le nœud Recipe est le seul élément d'un tableau JSON-LD de
    premier niveau
    ([{...}]), pas un objet nu.
  • Vérifié contre le code réel (json-ld-recipe.ts:79-92) que ce tableau
    de premier niveau est déjà géré tel quel par findRecipeNode (boucle sur
    Array.isArray(node) avant de chercher @type === "Recipe") →
    jsonLdRecipeAdapter.parse devrait fonctionner sans aucune retouche sur
    les pages Quitoque
    , exactement comme Marmiton (délégation intégrale,
    zéro patch — contrairement à 750g/Manger Bouger qui corrigent chacun un
    vrai bug du JSON-LD de leur site). extractPortions gère déjà "2"
    (regex \d+) ; flattenInstructions gère déjà HowToStep[] (prend
    .text, donc le titre "1. Les légumes" de chaque étape sera
    silencieusement ignoré — comportement identique pour n'importe quel
    autre site, pas une régression propre à Quitoque).
  • Page listing (/recettes) : 2711 recettes, paginée. Contrairement à
    Marmiton, pas de ItemList JSON-LD sur cette page (vérifié : seul
    Organization y est présent) — le pattern Marmiton (lire un ItemList
    embarqué) ne s'applique donc pas tel quel. En revanche :
    • Pagination confirmée : /recettes?page=2 renvoie une page
      différente (mêmes "2711 produits" annoncés, second lot de cartes) — un
      simple paramètre ?page=N, comme le &page=N de Marmiton.
    • Chaque carte est directement scrapable, plus simple encore que
      750g (qui doit apparier un <img> et un <a class="card-link">
      voisins) : un seul élément <a class="card__content_header" href="/recettes/<slug>"> contient à la fois le lien et un
      <img alt="<titre réel>" data-src="<url image réelle>"> — ⚠️ l'image
      réelle est dans data-src, src n'est qu'un placeholder lazy-load.
    • Pas de recherche texte confirmée : ni de champ "rechercher" dans le
      DOM, ni d'effet d'un ?q=poulet testé en direct (résultats strictement
      identiques à la page non filtrée) — seul un filtre par tags visible
      ("Filtrer" : Végétarien, Express, Kids friendly...), pas une recherche
      libre. Lacune de même nature que celle déjà assumée/documentée pour
      750g — pas un blocage, juste à documenter pareillement pour Quitoque.
    • Fin de liste : pas besoin d'un total fiable à l'avance — comme
      Marmiton, une page qui ne renvoie aucune carte est le signal de fin
      (nextCursor: null).

Conception proposée

Nouveau fichier apps/api/src/sources/quitoque.ts, calqué sur
marmiton.ts :

export const quitoqueAdapter: RecipeSourceAdapter<{ html: string; url: string }> = {
  key: "quitoque",
  name: "Quitoque",
  official: false, // même raisonnement que jsonLdRecipeAdapter/marmiton/750g/mangerBouger :
                    // lire le JSON-LD d'un site tiers n'est pas une API maintenue/publiée
  iconUrl: "https://www.quitoque.fr/favicon.ico", // à confirmer à l'implémentation
  locale: "fr",

  async list(params) {
    // Scrape /recettes?page=N (page=1 implicite si cursor absent) : un
    // regex par carte <a class="card__content_header" href="...">...
    // <img alt="..." data-src="...">, dans l'esprit de extractRecipeCards
    // de 750g.ts mais plus simple (tout est dans un seul élément conteneur,
    // pas besoin d'apparier deux nœuds voisins).
    // Si params.query est fourni : filtre les cartes de CETTE page par
    // sous-chaîne sur le titre (alt) avant de les renvoyer — pas de vraie
    // recherche serveur trouvée sur le site, comportement "best effort"
    // assumé et documenté comme tel (même esprit que le commentaire de
    // 750g sur sa propre lacune de recherche).
    // nextCursor: null si la page renvoie 0 carte, sinon String(page + 1)
    // — même logique que marmiton.ts:167-173.
  },

  async fetchDetail(externalId) {
    // Délègue tel quel à jsonLdRecipeAdapter.fetchDetail (externalId =
    // l'URL canonique renvoyée par list()), avec rekeySourceError comme
    // marmiton.ts:188-197 pour que sourceKey soit "quitoque" et non
    // "jsonLdRecipe" dans les erreurs remontées à l'utilisateur.
  },

  parse(raw) {
    // Délègue tel quel à jsonLdRecipeAdapter.parse (voir vérification
    // ci-dessus : aucune retouche nécessaire), même wrapping
    // rekeySourceError que marmiton.ts:199-205.
  },
};

rekeySourceError : dupliquer le petit helper privé de marmiton.ts:73-81
(pas exporté du module — 750g et Manger Bouger en ont chacun leur propre
copie aujourd'hui ; pas la peine de le factoriser pour ce ticket).

Ingrédients : recipeIngredient alimente ParsedRecipeIngredient.rawText/
.name tel quel (comme Marmiton/750g/Manger Bouger) — la résolution réelle
vers le catalogue (matchIngredientName/matchUnit/extractQuantity,
ingredient-matcher.ts, déjà générique et déjà éprouvé en français par les
3 adaptateurs existants) se fait plus loin dans le pipeline
(previewSourceItem, sources.service.ts), pas dans l'adaptateur — rien de
nouveau à construire là. Deux formulations Quitoque à surveiller (pas à
corriger dans l'adaptateur, juste à vérifier une fois de vraies recettes
importées) : le qualificatif entre parenthèses ("(Bio)") et une quantité en
plage ("1 à 3 cm gingembre") — au pire ces lignes finissent en ingrédient
non résolu, dans le flux de revue manuelle déjà existant
(RecipeImportForm/tryBuildCompleteImport), pas un blocage.

Fichiers à toucher

  • apps/api/src/sources/quitoque.ts (nouveau) — l'adaptateur ci-dessus.
  • apps/api/src/sources/index.ts — ajoute registerRecipeSource(quitoqueAdapter)
    dans registerAllRecipeSources().
  • apps/api/test/recipe-sources/quitoque.test.ts (nouveau) — même
    convention stubFetchHtml/globalThis.fetch que marmiton.test.ts/
    750g.test.ts/manger-bouger.test.ts (Mocha + Chai, pas de lib de mock
    HTTP). Cas à couvrir : list() parse bien les cartes d'une page HTML
    fixture, gère une page vide (fin de liste), filtre par query ;
    fetchDetail/parse délèguent bien et re-labellisent sourceKey en cas
    d'erreur ; parse sur une fixture JSON-LD réelle (tableau de premier
    niveau, forme Quitoque) produit les bons portions/ingredients/steps.
  • specs/backend-architecture.md — ajoute la section Quitoque au même
    format que les autres adaptateurs (sans corriger au passage la
    description déjà obsolète des 3 autres — voir la note en Contexte).

Hors scope

  • Recherche serveur fiable sur Quitoque, si le site en expose une un jour
    (aucune trouvée à ce stade).
  • Généricisation du scraping de cartes façon "un helper partagé" — seulement
    si un futur 6e site en a besoin ; pour l'instant chaque adaptateur scrapé
    garde sa propre logique de list(), cohérent avec 750g/Manger Bouger.
  • Rattrapage de la documentation obsolète de Marmiton/750g/Manger Bouger
    dans specs/backend-architecture.md (voir note en Contexte).

🤖 Filed by Claude Code

## Contexte Ajouter https://www.quitoque.fr/recettes comme nouvelle source de recettes importable, au même titre que TheMealDB/Marmiton/750g/Manger Bouger. Ce n'est **pas** un cas neuf : le repo a déjà 4 adaptateurs enregistrés (`apps/api/src/sources/`) — TheMealDB (API officielle) et **trois sites français scrapés** (Marmiton, 750g, Manger Bouger), tous construits sur un même bloc générique `jsonLdRecipeAdapter` (`json-ld-recipe.ts`) qui lit les données structurées `schema.org/Recipe` que la plupart des sites de recettes embarquent en JSON-LD pour Google. Le précédent le plus proche pour Quitoque est **Marmiton** (`marmiton.ts`) — le plus fin des 3 wrappers scrapés : `fetchDetail`/`parse` délégués tels quels au bloc générique, seul `list()` étant spécifique au site. (Note en passant, hors sujet de ce ticket : `specs/backend-architecture.md` — section "Sources externes", ~lignes 500-600 — est obsolète, elle ne documente que TheMealDB + le bloc générique et affirme à tort que `registerAllRecipeSources()` "n'enregistre aujourd'hui que TheMealDB". Le vrai code (`apps/api/src/sources/index.ts`) enregistre déjà les 4. Ne pas mélanger ce rattrapage avec ce ticket — seule la section Quitoque doit y être ajoutée ici.) ## Vérifications effectuées sur quitoque.fr Vérifié en direct (navigateur) plutôt que deviné : - **robots.txt** (`https://www.quitoque.fr/robots.txt`) : `/recettes` n'apparaît dans aucun `disallow` — autorisé pour `user-agent: *` **et** explicitement listé `allow: /` pour `ClaudeBot`/`GPTBot`/`PerplexityBot`/ `Google-Extended` nommément. Aucun des 3 adaptateurs scrapés existants ne documente une vérification robots.txt/CGU — celle-ci va donc plus loin que le précédent du repo, dans le bon sens. - **Page détail d'une recette** (`/recettes/<slug>`) : contient un bloc `<script type="application/ld+json">` avec un `Recipe` schema.org complet et propre (vérifié sur "Mijoté de lentilles vertes au lait de coco et chou romanesco") : `name`, `description`, `image`, `prepTime`/`totalTime` (ISO 8601), `recipeYield: "2"`, `recipeIngredient: ["150 g lentilles vertes (Bio)", "1 gousse d'ail", "0.5 citron jaune (Bio)", "1 à 3 cm gingembre", ...]`, `recipeInstructions` en `HowToStep[]` (`name` + `text` par étape). Le nœud `Recipe` est le seul élément d'un **tableau JSON-LD de premier niveau** (`[{...}]`), pas un objet nu. - **Vérifié contre le code réel** (`json-ld-recipe.ts:79-92`) que ce tableau de premier niveau est déjà géré tel quel par `findRecipeNode` (boucle sur `Array.isArray(node)` avant de chercher `@type === "Recipe"`) → **`jsonLdRecipeAdapter.parse` devrait fonctionner sans aucune retouche sur les pages Quitoque**, exactement comme Marmiton (délégation intégrale, zéro patch — contrairement à 750g/Manger Bouger qui corrigent chacun un vrai bug du JSON-LD de leur site). `extractPortions` gère déjà `"2"` (regex `\d+`) ; `flattenInstructions` gère déjà `HowToStep[]` (prend `.text`, donc le titre "1. Les légumes" de chaque étape sera silencieusement ignoré — comportement identique pour n'importe quel autre site, pas une régression propre à Quitoque). - **Page listing** (`/recettes`) : 2711 recettes, paginée. Contrairement à Marmiton, **pas de `ItemList` JSON-LD** sur cette page (vérifié : seul `Organization` y est présent) — le pattern Marmiton (lire un `ItemList` embarqué) ne s'applique donc pas tel quel. En revanche : - **Pagination confirmée** : `/recettes?page=2` renvoie une page différente (mêmes "2711 produits" annoncés, second lot de cartes) — un simple paramètre `?page=N`, comme le `&page=N` de Marmiton. - **Chaque carte est directement scrapable**, plus simple encore que 750g (qui doit apparier un `<img>` et un `<a class="card-link">` voisins) : un seul élément `<a class="card__content_header" href="/recettes/<slug>">` contient à la fois le lien et un `<img alt="<titre réel>" data-src="<url image réelle>">` — ⚠️ l'image réelle est dans `data-src`, `src` n'est qu'un placeholder lazy-load. - **Pas de recherche texte confirmée** : ni de champ "rechercher" dans le DOM, ni d'effet d'un `?q=poulet` testé en direct (résultats strictement identiques à la page non filtrée) — seul un filtre par tags visible ("Filtrer" : Végétarien, Express, Kids friendly...), pas une recherche libre. Lacune de même nature que celle déjà assumée/documentée pour 750g — pas un blocage, juste à documenter pareillement pour Quitoque. - **Fin de liste** : pas besoin d'un total fiable à l'avance — comme Marmiton, une page qui ne renvoie aucune carte est le signal de fin (`nextCursor: null`). ## Conception proposée Nouveau fichier `apps/api/src/sources/quitoque.ts`, calqué sur `marmiton.ts` : ```ts export const quitoqueAdapter: RecipeSourceAdapter<{ html: string; url: string }> = { key: "quitoque", name: "Quitoque", official: false, // même raisonnement que jsonLdRecipeAdapter/marmiton/750g/mangerBouger : // lire le JSON-LD d'un site tiers n'est pas une API maintenue/publiée iconUrl: "https://www.quitoque.fr/favicon.ico", // à confirmer à l'implémentation locale: "fr", async list(params) { // Scrape /recettes?page=N (page=1 implicite si cursor absent) : un // regex par carte <a class="card__content_header" href="...">... // <img alt="..." data-src="...">, dans l'esprit de extractRecipeCards // de 750g.ts mais plus simple (tout est dans un seul élément conteneur, // pas besoin d'apparier deux nœuds voisins). // Si params.query est fourni : filtre les cartes de CETTE page par // sous-chaîne sur le titre (alt) avant de les renvoyer — pas de vraie // recherche serveur trouvée sur le site, comportement "best effort" // assumé et documenté comme tel (même esprit que le commentaire de // 750g sur sa propre lacune de recherche). // nextCursor: null si la page renvoie 0 carte, sinon String(page + 1) // — même logique que marmiton.ts:167-173. }, async fetchDetail(externalId) { // Délègue tel quel à jsonLdRecipeAdapter.fetchDetail (externalId = // l'URL canonique renvoyée par list()), avec rekeySourceError comme // marmiton.ts:188-197 pour que sourceKey soit "quitoque" et non // "jsonLdRecipe" dans les erreurs remontées à l'utilisateur. }, parse(raw) { // Délègue tel quel à jsonLdRecipeAdapter.parse (voir vérification // ci-dessus : aucune retouche nécessaire), même wrapping // rekeySourceError que marmiton.ts:199-205. }, }; ``` `rekeySourceError` : dupliquer le petit helper privé de `marmiton.ts:73-81` (pas exporté du module — 750g et Manger Bouger en ont chacun leur propre copie aujourd'hui ; pas la peine de le factoriser pour ce ticket). **Ingrédients** : `recipeIngredient` alimente `ParsedRecipeIngredient.rawText`/ `.name` tel quel (comme Marmiton/750g/Manger Bouger) — la résolution réelle vers le catalogue (`matchIngredientName`/`matchUnit`/`extractQuantity`, `ingredient-matcher.ts`, déjà générique et déjà éprouvé en français par les 3 adaptateurs existants) se fait plus loin dans le pipeline (`previewSourceItem`, `sources.service.ts`), pas dans l'adaptateur — rien de nouveau à construire là. Deux formulations Quitoque à surveiller (pas à corriger dans l'adaptateur, juste à vérifier une fois de vraies recettes importées) : le qualificatif entre parenthèses ("(Bio)") et une quantité en plage ("1 à 3 cm gingembre") — au pire ces lignes finissent en ingrédient non résolu, dans le flux de revue manuelle déjà existant (`RecipeImportForm`/`tryBuildCompleteImport`), pas un blocage. ## Fichiers à toucher - `apps/api/src/sources/quitoque.ts` (nouveau) — l'adaptateur ci-dessus. - `apps/api/src/sources/index.ts` — ajoute `registerRecipeSource(quitoqueAdapter)` dans `registerAllRecipeSources()`. - `apps/api/test/recipe-sources/quitoque.test.ts` (nouveau) — même convention `stubFetchHtml`/`globalThis.fetch` que `marmiton.test.ts`/ `750g.test.ts`/`manger-bouger.test.ts` (Mocha + Chai, pas de lib de mock HTTP). Cas à couvrir : `list()` parse bien les cartes d'une page HTML fixture, gère une page vide (fin de liste), filtre par `query` ; `fetchDetail`/`parse` délèguent bien et re-labellisent `sourceKey` en cas d'erreur ; `parse` sur une fixture JSON-LD réelle (tableau de premier niveau, forme Quitoque) produit les bons `portions`/`ingredients`/`steps`. - `specs/backend-architecture.md` — ajoute la section Quitoque au même format que les autres adaptateurs (sans corriger au passage la description déjà obsolète des 3 autres — voir la note en Contexte). ## Hors scope - Recherche serveur fiable sur Quitoque, si le site en expose une un jour (aucune trouvée à ce stade). - Généricisation du scraping de cartes façon "un helper partagé" — seulement si un futur 6e site en a besoin ; pour l'instant chaque adaptateur scrapé garde sa propre logique de `list()`, cohérent avec 750g/Manger Bouger. - Rattrapage de la documentation obsolète de Marmiton/750g/Manger Bouger dans `specs/backend-architecture.md` (voir note en Contexte). --- 🤖 Filed by [Claude Code](https://claude.com/claude-code)
kyuno added the
enhancement
planned
labels 2026-09-05 15:46:05 +02:00
kyuno added the
Medium
label 2026-09-05 18:51:46 +02:00
kyuno closed this issue 2026-09-06 01:28:04 +02:00
Sign in to join this conversation.
No milestone
No project
No assignees
1 participant
Notifications
Due date
The due date is invalid or out of range. Please use the format "yyyy-mm-dd".

No due date set.

Dependencies

No dependencies set.

Reference: kyuno/batchCooking#37
No description provided.