La clé est toujours ['api', controller, operation, input] : même input, même entrée de cache — quel que soit l’endroit du code.
1.3configuration globale
defineQueryConfig — une fois par app, pour toutes les queries.
Le moteur est commun ; chaque app décide fraîcheur, retry, invalidation, préchauffage et persistance. Les écrans n’en savent rien.
1
mutationInvalidationaprès une mutation, quelles queries deviennent périmées — par défaut le contrôleur, avec exceptions.
2
queryClient · defaultOptionsle TanStack classique : staleTime, gcTime, focus, retry par type d’erreur.
3
warmupqueries lancées après la première page, en priorité basse — les référentiels sont chauds quand un formulaire s’ouvre.
4
persistencebrancher un persister (IndexedDB) et une liste blanche — le cache survit à la fermeture.
1.3configuration globale · les quatre apps
Même contrat, quatre politiques.
Ce que chaque config/query.ts a choisi, et pourquoi.
customer-front
Transactionnel authentifié : rafraîchir vite, sans churn au focus.
staleTime15 s
gcTime15 min
focus refetchnon
retry1
invalidationcontroller
admin-front
Poste de travail dense : revenir sur l’onglet montre l’état récent.
staleTime5 s
gcTime10 min
focus refetchoui
retry≥ 500 · 1×
warmup3 référentiels · 30 min
market-front
Public, SSR : ne jamais rejouer une 404, ne pas refetch au remontage.
staleTime30 s
gcTime∞ serveur · 30 min
retryOnMountnon
retry408 · 429 · ≥ 500
prefetchplans de route
inventory-front
PWA terrain : référentiel disponible 24 h sans réseau.
staleTime24 h
reconnexionrefetch
retry1 · mutations 3
persistanceIndexedDB
scopesession × shop
1.3configuration globale · intégrations
Les intégrations sont des capacités, pas des plugins.
Désactivé par défaut dans la couche partagée. Une app active ; le plugin propriétaire lit la config avant d’injecter le moindre script.
capacitécustomeradminmarketinventory
googleTagManager✓–✓–
didomi · consentement✓–✓–
recaptcha✓✓✓✓
googlePlaces✓✓––
markerIo · feedback✓✓✓–
0coût nul pour ce qu’on n’active pas
Inventory ne paie ni analytics, ni consentement, ni Places, ni feedback — et garde reCAPTCHA. Étendre la couche partagée ne veut pas dire charger tout ce qu’elle contient.
1.4consommer · useQuery
Notre useQuery : TanStack, plus ce qui manquait.
Un getter d’options, un second argument, un retour étendu — la même signature dans les quatre apps.
①
getterinput ou enabled changent → autre clé, autre requête. Pas de watch, pas de refetch().
②
defaultValue · pageContexttype sans undefined ; l’entité alimente fil d’Ariane et titre.
②
onData · onErrordéclenchés une fois par mise à jour du cache — pas à chaque render, pas au remontage sur données fraîches.
⇠
retour étendupatchData, setData, suspense… visent l’entrée exacte qu’on lit — la clé n’est jamais recalculée.
1.4consommer · useQuery · en situation
Six choses qu’on n’écrit plus à la main.
Extraits réels des apps — un onglet par capacité. Chacun remplace un watch, un store ou un refetch d’autrefois.
1
onDatadériver un état local, une fois par donnée — plus de watch à protéger.
2
onErrordéconnecter, capturer dans l’error boundary.
3
pageContextfil d’Ariane et titre dérivés de l’entité.
4
patchDatapousser le résultat d’une écriture — plus de commit + rechargement.
5
data en écritureun live update écrit dans le cache — tous les observateurs suivent.
6
suspense()attendre la 1ʳᵉ résolution — SSR, préchargement hors-ligne.
1.4consommer · tables, formulaires, catalogue
Un queryOptions, six consommateurs.
Partout où une donnée entre, c’est la même déclaration — donc la même clé, le même cache, la même invalidation.
1.5mutations
Écrire, puis laisser invalider.
Avant : le code appelant décide quoi recharger. Après : le contrôleur de la mutation devient périmé, selon la politique de l’app.
1.6prefetch intelligent
La page déclare ses données. Le lien les charge.
Au survol d’un lien, Nuxt exécute le plan de la page destination dans le même cache que useQuery lira à l’arrivée.
1.6prefetch intelligent · les formes
Une option, une liste, ou un plan.
Le nom de route type les params ; le contexte donne client et route (plus prismic sur market-front).
dédoublonné par fullPathchunk et données en parallèleun plan en échec n’empêche jamais la navigation
02
Marketplace
Ce qui n’existe que sur market-front.
2.1prefetch · en production
Chaque page déclare son plan. Chaque lien le lance.
Le résultat mesuré en production, et un plan de page réel : entité + catalogue préparés ensemble.
index · catalogue · catégorie · séance · vendeur · produit · Prismictoutes les pages ont un plan
2.2CatalogList
Un DataList pour la découverte : la même famille, d’autres clés.
Ce que chaque zone de la page catalogue doit à une clé de defineCatalogSchema. En bleu : ce qui n’existe pas côté DataList.
1
Catalogue1 284 résultatsTrier : fin d’enchère ↑
2
Catégorie
Manutention 312
Machines-outils 208
Véhicules 144
Prix
Localisation
France 1 102
Belgique 182
3
Chariot élévateur Toyota6 200 €
Presse hydraulique 40 t1 450 €
Tour CNC Haas ST-1018 900 €
Compresseur Atlas Copco2 300 €
Nacelle Haulotte7 800 €
Pelle Kubota U2715 200 €
4
‹123…54›/catalogue/page2
5
clé du schéma → zone
1#top · #toolbar
slots : titre, total, tri, sauvegarder la recherche
2filters + source.facets
les filtres, alimentés par les agrégats de la recherche
3renderItem · gridSize
({ row }) => <ItemCard item={row} /> — une carte par ligne, pas de colonnes
4pagination.route
segment de chemin indexable, pas de query string
5context · runtimeContextType
vendeur / catégorie résolus avant la requête · contexte d’écran typé
behavior.onFilterChange
renvoyer une route — changer de catégorie ramène au catalogue
Le composant, le SSR et le prefetch lisent la même définition — un filtre change une fois.
2.4SEO
Deux composables. Tout dérive de l’entité.
useMarketPageSeo pour une page, useMarketItemSeo pour une fiche : six sorties réactives.
réactif : le prix change → l’offre Schema.org et la carte OG changent
2.5images OG
Une image de partage = un composant Vue.
defineOgImage reçoit des props réactives ; le composant Takumi est rendu côté serveur en JPG.
AGORASTORE
Manutention · Chariots élévateurs
Chariot élévateur électrique Toyota 8FBE18, 2019
6 200 €enchère en cours
rendu serveur → JPG 1200×630cache 7 joursMarket.takumi pour les pages, Product.takumi pour les fichesnouveau type de page = un composant + un defineOgImage
2.6modules Nuxt
Un module par corvée. On configure, on ne code pas.
Ce que chaque module sert en plus de la page — robots, sitemaps, images OG, JSON-LD, llms.txt — sans une ligne de plomberie dans market-front.
defineOgImage('Product.takumi', props) — rendu serveur en JPG 1200×630, cache 7 jours, images restreintes à l’origine. Un nouveau type de page : un composant Vue de plus.
Content-Signal : recherche et usage à la demande autorisés, entraînement refusé. Un llms.txt par tenant — catalogue, catégories, index complet, et la règle : « la page fait foi ».
Locales par domaine, préfixe de langue, chemins traduits : /fr/produit, /nl/product, /de/produkt. Mode strictSeo : hreflang et canonical toujours cohérents.
hreflangx-defaultdefineI18nRoute()
⚡
nuxt-vitalizer
Moins de préchargements, meilleur LCP
disablePreloadLinks : plus de cascade de <link rel=preload> qui concurrence l’image principale. Combiné au prefetch des liens à l’interaction plutôt qu’à la visibilité.
prefetchOn: interaction0 preload chunks
ui
dompurify · lazytube · carousel
Le reste du poids lourd
v-sanitize-html sur le HTML venu des traductions et du CMS. <LazyYoutube> : la vidéo de la galerie ne charge rien avant le clic. Carrousels prêts à l’emploi sur huit écrans.
v-sanitize-html<LazyYoutube><Carousel>
03
Entity Details
La fiche entité : ce que c’est, comment elle s’orchestre, comment on en écrit une. Puis deux briques de plus : sélecteurs distants, table en pièces détachées.
3.1qu’est-ce que c’est
Une fiche = un en-tête, des sections, un rail.
Organisation, item, séance, contrat, facture, utilisateur : la même structure, opérée par un composant partagé — <EntityDetailShell>.
formulaires de section · pour les relations : le <y>TableSchema({ <x>ShortId }) existant + son embed
3
Config + shell
components/entities/<x>/detail/<X>Details.vue
defineEntityDetails (header · groupes) · <EntityDetailShell> · un slot par section sur mesure
4
La route
pages/<x>/[id].vue
definePageMeta — accès, fil d’Ariane via ctx.detailLabel — et le composant. Rien d’autre.
déjà sur ce modèle : contrats · factures · items · organisations · séances · utilisateurstests montés : packages/shared-ui/test/entity-details
3.11en pratique · où va le code
Un dossier par entité. Un fichier par rôle.
Jamais pour un seul appelant : pas d’helper, de constante ou de type créé pour un consommateur unique. Une déclaration réutilisable vit chez son propriétaire.
entities/organisations/
├── schema.tsxliste + bloc embed
├── action.tsopérations déclenchées par l’utilisateur
├── render.tsxcellules, statuts
├── filter.tsfiltres du domaine
├── utils.ts · constants.ts · types.tsvaleurs et types canoniques
└── detail/
├── schema.tsxsections éditables
├── action.tsactions de la fiche
├── render.tsxadaptateurs de champs custom
└── loader.tsle loader étagé — et le plan de prefetch
?« où est-ce que ça va ? »
Une colonne → render.tsx. Un filtre → filter.ts. Une action de ligne → action.ts. Une section de fiche → detail/schema.tsx. Un chargement → detail/loader.ts.
↺corriger le propriétaire
Un comportement faux dans une table embarquée se corrige dans le schéma canonique — la projection suit. Idem pour un champ distant : dans shared-business/entities/…/fields.tsx.
28dossiers d’entité en admin
Le même plan pour chacun. Les sous-dossiers (create/, merge/, users/…) tiennent les écrans satellites.
3.12autres briques · options distantes du moteur de formulaire
Un champ qui cherche dans toute la base.
L’admin opère sur tout le jeu de données : les sélecteurs paginent côté serveur. Le champ déclare deux requêtes, le moteur fait le reste.