Sources externes : Manger Bouger sans filtre s'arrête à la première page #78

Closed
opened 2026-09-06 23:58:40 +02:00 by kyuno · 0 comments
Owner

Contexte

En parcourant la source Manger Bouger dans l'onglet Sources
(/recettes) sans filtre texte, le défilement infini s'arrête après la
première page (9 recettes) au lieu de dérouler tout le catalogue de la
Fabrique à Menus (plusieurs milliers de recettes). Constaté sur l'app
déployée.

Avec un filtre texte (?query=<mot-clé>), la pagination fonctionne
normalement — seul le parcours « tout voir » (filtre vide) est touché.

L'adaptateur apps/api/src/sources/manger-bouger.ts cible déjà le bon
schéma d'URL (?diet=ALL&page=N), celui que mangerbouger.fr pagine
correctement :
https://www.mangerbouger.fr/manger-mieux/la-fabrique-a-menus/recettes?diet=ALL&page=1

Ce qui a été vérifié

manger-bouger.ts:287-288 construit toujours l'URL avec un paramètre
query final, même vide :

const query = params.query ?? "";               // filtre vide => ""
const listUrl = `${LIST_URL}?diet=ALL&page=${page}&query=${encodeURIComponent(query)}`;
// filtre vide, page 1 => ...recettes?diet=ALL&page=1&query=   (&query= vide en fin)

(browseSourceSchema, packages/shared/src/schemas/sources.ts:4-7, fait
arriver le filtre vide en undefined ; l'adaptateur le transforme en ""
et ajoute quand même &query=.)

Cette URL exacte (?diet=ALL&page=N&query=) a été testée en direct depuis
trois clients — navigateur, curl, et le fetch natif de Node 22 (le
runtime de l'API) — sur les pages 1, 2, 3, 5, 10, 25, 50, 100 :

  • 9 recettes distinctes par page, aucun recouvrement entre pages ;
  • hasMorePages: true jusqu'au bout, puis hasMorePages: false + liste
    vide au-delà de la dernière page (fin propre).

Depuis une machine de dev, l'URL avec &query= vide pagine donc
correctement. Le blocage n'est reproductible que sur le serveur
déployé — la raison serveur/CDN exacte reste une hypothèse (voir plus
bas), mais le correctif n'a pas besoin de la trancher.

Côté front, le garde-fou anti-boucle ajouté pour l'issue #38
(apps/web/src/features/recipes/sources/RecipeSourcesPanel.tsx:239-254)
traite « cette page n'apporte aucun externalId nouveau » comme une fin de
liste (nextCursor: null) :

const seen = new Set(prev.items.map((item) => item.externalId));
const freshItems = items.filter((item) => !seen.has(item.externalId));
if (freshItems.length === 0) {
  return { status: "loaded", items: prev.items, nextCursor: null };
}

Si, côté serveur déployé, la page 2 du parcours &query= vide revient
identique à la page 1 (cache CDN qui normalise le query= vide et sert
la page 1 pour tout page=N, ou rendu serveur qui ignore page en
l'absence de query réel), alors freshItems est vide, ce garde-fou coupe
le défilement, et on ne voit que 9 recettes. Le garde-fou fait son travail
— la cause est en amont, dans la requête envoyée par l'adaptateur.

Cause

L'adaptateur envoie un &query= vide superflu sur le parcours sans filtre,
au lieu de la requête « catalogue » nue ?diet=ALL&page=N que
mangerbouger.fr pagine réellement. Aucun des autres adaptateurs scrapés
n'ajoute un paramètre de recherche vide à un endpoint paginé —
marmiton.ts:117-121, quitoque.ts:114-141 et ohmonbento.ts:100-104
conditionnent le paramètre ; seul manger-bouger.ts l'ajoute
systématiquement.

Correctif proposé

manger-bouger.ts, list() — n'ajouter &query= que si le filtre est non
vide, comme les adaptateurs voisins :

const query = (params.query ?? "").trim();
const listUrl = `${LIST_URL}?diet=ALL&page=${page}${
  query ? `&query=${encodeURIComponent(query)}` : ""
}`;

diet=ALL reste (présent aussi dans l'URL de référence). Le chemin filtré
(&query=<mot-clé>) ne change pas — il fonctionne déjà et doit continuer.

Corriger aussi les commentaires de LIST_URL (l.16-25) et de
hasMorePages (l.45) : ils affirment une pagination « vérifiée en direct »
sans préciser que ça ne valait que pour le chemin avec query texte.

Fichiers à toucher

  • apps/api/src/sources/manger-bouger.ts — list() : paramètre query
    conditionnel ; commentaires LIST_URL / hasMorePages mis à jour.
  • apps/api/test/recipe-sources/manger-bouger.test.ts — le
    describe("list") (l.84-185) n'a aucun cas sans query. Ajouter :
    • list({}) (filtre absent) construit une URL sans paramètre query
      (pas de &query= final) — modèle : le test de capture d'URL l.120-133
      (requestedUrl) + les helpers stubFetchHtml / htmlWithListNextData.
    • list({}) renvoie un nextCursor non nul quand __NEXT_DATA__ porte
      hasMorePages: true.
    • list({ query: "salade" }) ajoute toujours &query=salade (non
      régression du chemin filtré).

Vérification

  1. Reproduire sur le serveur déployé, avant/après : depuis le conteneur
    api, curl sur …/recettes?diet=ALL&page=1&query= puis &page=2,
    comparer les slug du __NEXT_DATA__ (recouvrement = bug confirmé) ;
    refaire sans le &query=.
  2. pnpm --filter @batch-cooking/api test (Mocha) — nouveaux cas verts.
  3. App déployée : onglet Sources → Manger Bouger, sans filtre → le
    défilement dépasse la première page et déroule le catalogue ; avec un
    filtre → toujours OK.

Hors scope

  • Le garde-fou freshItems.length === 0 de RecipeSourcesPanel.tsx
    (issue #38) : correct tel quel, il cesse simplement de se déclencher une
    fois la requête corrigée.
  • La dégradation silencieuse générale de list() quand __NEXT_DATA__ est
    absent ({ items: [], nextCursor: null } sans rien signaler,
    manger-bouger.ts:66-74 + l.306) — pas la cause ici (la page 2 revient
    avec des items, juste dupliqués) ; à traiter séparément si besoin.
  • La section « Adaptateurs concrets » de specs/backend-architecture.md,
    déjà obsolète pour Marmiton/750g/Manger Bouger (aucun n'y a de puce
    dédiée) — rattrapage global à part, cf. issue #37.

🤖 Filed by Claude Code

## Contexte En parcourant la source **Manger Bouger** dans l'onglet Sources (`/recettes`) **sans filtre texte**, le défilement infini s'arrête après la première page (9 recettes) au lieu de dérouler tout le catalogue de la Fabrique à Menus (plusieurs milliers de recettes). Constaté sur l'app déployée. Avec un filtre texte (`?query=<mot-clé>`), la pagination fonctionne normalement — seul le parcours « tout voir » (filtre vide) est touché. L'adaptateur `apps/api/src/sources/manger-bouger.ts` cible déjà le bon schéma d'URL (`?diet=ALL&page=N`), celui que `mangerbouger.fr` pagine correctement : `https://www.mangerbouger.fr/manger-mieux/la-fabrique-a-menus/recettes?diet=ALL&page=1` ## Ce qui a été vérifié `manger-bouger.ts:287-288` construit **toujours** l'URL avec un paramètre `query` final, même vide : const query = params.query ?? ""; // filtre vide => "" const listUrl = `${LIST_URL}?diet=ALL&page=${page}&query=${encodeURIComponent(query)}`; // filtre vide, page 1 => ...recettes?diet=ALL&page=1&query= (&query= vide en fin) (`browseSourceSchema`, `packages/shared/src/schemas/sources.ts:4-7`, fait arriver le filtre vide en `undefined` ; l'adaptateur le transforme en `""` et ajoute quand même `&query=`.) Cette URL exacte (`?diet=ALL&page=N&query=`) a été testée en direct depuis trois clients — navigateur, `curl`, et le `fetch` natif de Node 22 (le runtime de l'API) — sur les pages 1, 2, 3, 5, 10, 25, 50, 100 : - 9 recettes distinctes par page, **aucun recouvrement** entre pages ; - `hasMorePages: true` jusqu'au bout, puis `hasMorePages: false` + liste vide au-delà de la dernière page (fin propre). Depuis une machine de dev, l'URL avec `&query=` vide pagine donc **correctement**. Le blocage n'est reproductible que sur le serveur déployé — la raison serveur/CDN exacte reste une hypothèse (voir plus bas), mais le correctif n'a pas besoin de la trancher. Côté front, le garde-fou anti-boucle ajouté pour l'issue #38 (`apps/web/src/features/recipes/sources/RecipeSourcesPanel.tsx:239-254`) traite « cette page n'apporte aucun `externalId` nouveau » comme une fin de liste (`nextCursor: null`) : const seen = new Set(prev.items.map((item) => item.externalId)); const freshItems = items.filter((item) => !seen.has(item.externalId)); if (freshItems.length === 0) { return { status: "loaded", items: prev.items, nextCursor: null }; } Si, côté serveur déployé, la page 2 du parcours `&query=` vide revient **identique à la page 1** (cache CDN qui normalise le `query=` vide et sert la page 1 pour tout `page=N`, ou rendu serveur qui ignore `page` en l'absence de `query` réel), alors `freshItems` est vide, ce garde-fou coupe le défilement, et on ne voit que 9 recettes. Le garde-fou fait son travail — la cause est en amont, dans la requête envoyée par l'adaptateur. ## Cause L'adaptateur envoie un `&query=` vide superflu sur le parcours sans filtre, au lieu de la requête « catalogue » nue `?diet=ALL&page=N` que `mangerbouger.fr` pagine réellement. Aucun des autres adaptateurs scrapés n'ajoute un paramètre de recherche vide à un endpoint paginé — `marmiton.ts:117-121`, `quitoque.ts:114-141` et `ohmonbento.ts:100-104` conditionnent le paramètre ; seul `manger-bouger.ts` l'ajoute systématiquement. ## Correctif proposé `manger-bouger.ts`, `list()` — n'ajouter `&query=` que si le filtre est non vide, comme les adaptateurs voisins : const query = (params.query ?? "").trim(); const listUrl = `${LIST_URL}?diet=ALL&page=${page}${ query ? `&query=${encodeURIComponent(query)}` : "" }`; `diet=ALL` reste (présent aussi dans l'URL de référence). Le chemin filtré (`&query=<mot-clé>`) ne change pas — il fonctionne déjà et doit continuer. Corriger aussi les commentaires de `LIST_URL` (l.16-25) et de `hasMorePages` (l.45) : ils affirment une pagination « vérifiée en direct » sans préciser que ça ne valait que pour le chemin avec `query` texte. ## Fichiers à toucher - `apps/api/src/sources/manger-bouger.ts` — `list()` : paramètre `query` conditionnel ; commentaires `LIST_URL` / `hasMorePages` mis à jour. - `apps/api/test/recipe-sources/manger-bouger.test.ts` — le `describe("list")` (l.84-185) n'a **aucun** cas sans `query`. Ajouter : - `list({})` (filtre absent) construit une URL **sans** paramètre `query` (pas de `&query=` final) — modèle : le test de capture d'URL l.120-133 (`requestedUrl`) + les helpers `stubFetchHtml` / `htmlWithListNextData`. - `list({})` renvoie un `nextCursor` non nul quand `__NEXT_DATA__` porte `hasMorePages: true`. - `list({ query: "salade" })` ajoute toujours `&query=salade` (non régression du chemin filtré). ## Vérification 1. Reproduire sur le serveur déployé, avant/après : depuis le conteneur `api`, `curl` sur `…/recettes?diet=ALL&page=1&query=` puis `&page=2`, comparer les `slug` du `__NEXT_DATA__` (recouvrement = bug confirmé) ; refaire sans le `&query=`. 2. `pnpm --filter @batch-cooking/api test` (Mocha) — nouveaux cas verts. 3. App déployée : onglet Sources → Manger Bouger, sans filtre → le défilement dépasse la première page et déroule le catalogue ; avec un filtre → toujours OK. ## Hors scope - Le garde-fou `freshItems.length === 0` de `RecipeSourcesPanel.tsx` (issue #38) : correct tel quel, il cesse simplement de se déclencher une fois la requête corrigée. - La dégradation silencieuse générale de `list()` quand `__NEXT_DATA__` est absent (`{ items: [], nextCursor: null }` sans rien signaler, `manger-bouger.ts:66-74` + l.306) — pas la cause ici (la page 2 revient avec des items, juste dupliqués) ; à traiter séparément si besoin. - La section « Adaptateurs concrets » de `specs/backend-architecture.md`, déjà obsolète pour Marmiton/750g/Manger Bouger (aucun n'y a de puce dédiée) — rattrapage global à part, cf. issue #37. --- 🤖 Filed by [Claude Code](https://claude.com/claude-code)
kyuno added the
planned
bug
Medium
labels 2026-09-06 23:58:40 +02:00
kyuno closed this issue 2026-09-08 21:14:38 +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#78
No description provided.