Categories
TL;DR : un produit a un champ
category(texte libre). La page/catalogagrège la liste distincte des catégories et en fait des filter pills. Pas de table dédiée — choix volontaire pour rester léger sur les petits catalogues.

Concept
Trackily n'a pas de table categories séparée. À la place, native_products.category est un simple TEXT ajouté en v64 du schéma. La liste distincte des catégories est calculée à la demande, et les filter pills de /catalog sont construites à partir de cette liste.
Pourquoi pas de table relationnelle ? Parce que la majorité des opérateurs de commerce native font tourner des petits catalogues (5 à 30 produits). Maintenir une table de catégories + une table de jointure pour 12 produits, c'est du sur-engineering. Taper le nom de la catégorie dans le champ texte du produit est plus rapide, plus visible, et tu peux toujours migrer vers un schéma relationnel le jour où ton catalogue passe à 500 produits (la valeur étant déjà normalisée par LOWER() dans l'index, la migration est triviale).
Implémentation
ALTER TABLE native_products
ADD COLUMN IF NOT EXISTS category TEXT DEFAULT '';
CREATE INDEX idx_native_products_category
ON native_products(LOWER(category))
WHERE category <> '';
L'index partiel sur LOWER(category) rend les requêtes "filter par catégorie" instantanées même sur des catalogues plus gros, en case-insensitive.
Compatibilité avec les collections Shopify
L'écosystème Shopify dispose en plus des add_product_to_collection / create_collection / remove_product_from_collection MCP qui ciblent les vraies collections Shopify. Pour le natif, ces outils ne sont pas applicables — utilise le champ category à la place. Si tu fais tourner les deux mondes en parallèle, la doctrine est :
| Native | Shopify | |
|---|---|---|
| Regroupement | native_products.category (TEXT) |
Collections Shopify (table dédiée) |
| Filter front | /catalog?category=... |
Page collection Shopify |
| MCP | update_product patch category |
add_product_to_collection, remove_product_from_collection |
| Multi-catégorie ? | Non (1 produit = 1 catégorie) | Oui (N-N via collections) |
Comment faire (UI + MCP)
Via l'admin UI
- Fiche produit → champ Category.
- Tape le nom (par exemple "Suppléments cognitifs"). Auto-complete propose les catégories déjà utilisées sur d'autres produits (tu peux donc rester cohérent sans contrainte stricte).
- Save.
- Refresh
/catalog— la pill apparaît si au moins un produitstatus='active'est dans cette catégorie.
Via MCP
{
"name": "update_product",
"arguments": {
"platform": "native",
"product_id": 12,
"patch": { "category": "Suppléments cognitifs" }
}
}
Pour Shopify, le pattern collections :
{
"name": "create_collection",
"arguments": {
"store_id": 3,
"title": "Best sellers Été 2026",
"body_html": "<p>Nos produits les plus vendus en saison estivale.</p>",
"sort_order": "best-selling"
}
}
{
"name": "add_product_to_collection",
"arguments": {
"store_id": 3,
"product_id": 8721234567,
"collection_id": 41212345678
}
}
{
"name": "remove_product_from_collection",
"arguments": {
"store_id": 3,
"product_id": 8721234567,
"collection_id": 41212345678
}
}
Ces 3 outils sont Tier-2 (preview + confirm_token). Les collections Shopify sont visibles côté admin du store Shopify, pas côté admin Trackily.
La page /catalog en détail
/catalog est la vitrine publique de tes produits natifs. Trois utilisations principales :
- Vendre directement sans landing dédiée. Tu pousses ton trafic vers
/catalog, le visiteur browse, clique sur une carte, atterrit sur/p/<slug>, achète. - Servir de fallback sur un domaine sans
root_behaviorspécifique configuré. Les domaines avecroot_behavior='catalog'rendent ce template sur/. - Mini-store par catégorie :
/catalog?category=Boissonsfiltre la grille en client-side ET côté serveur (le?category=est utilisé pour le rendu initial SSR).
Structure du rendu
+----------------------------------------+
| Brand label | Shop all | | ← Header (depuis settings ou domain branding)
+----------------------------------------+
| |
| [Toutes 12] [Boissons 4] | ← Filter pills (catégories distinctes)
| [Snacks 3] [Compléments 5] |
| |
+----------------------------------------+
| ┌───────┐ ┌───────┐ ┌───────┐ |
| │ image │ │ image │ │ image │ | ← Grille produits (cards)
| │ Nom │ │ Nom │ │ Nom │ |
| │ 29 € │ │ 49 € │ │ 19 € │ |
| └───────┘ └───────┘ └───────┘ |
+----------------------------------------+
Chaque pill affiche le count entre parenthèses (produits actifs de cette catégorie). La pill "All" est par défaut active. Le click change l'URL en ?category=Boissons et re-render la grille.
Stratégie de naming
Quelques recommandations pratiques :
- Garde les noms courts — les pills ont un max-width, un nom trop long passe en ellipsis (frustrant).
- Pluriel ou singulier — choisis et reste cohérent. "Suppléments" partout, pas "Supplément" pour 1 et "Suppléments" pour les autres (Trackily est case-insensitive mais pas pluriel-insensitive).
- Une seule langue par catalog. Si tu vends en multi-langue, fais un produit par langue avec une catégorie par langue. Pas de routing automatique sur la pill.
- Évite les emojis dans la catégorie — ça mange de la place, ça casse l'URL
?category=. Garde-les pour le nom du produit si tu veux du visuel.
Exemples concrets
1. Mini-store de 12 produits, 3 catégories
Boissons (4 produits)
- Smoothie Énergie Matin
- Smoothie Détox Soir
- Boost Café Cordyceps
- Tisane Sommeil Profond
Snacks (3 produits)
- Barres Protéines Cacao
- Barres Protéines Beurre de Cacahuète
- Crackers Graines Bio
Compléments (5 produits)
- Memo Mind (3 variantes)
- Sleep Mind (2 variantes)
- Joint Care Pro
- Vitamine D3 Lipo
- Omega-3 Cure
Côté MCP, tu peux scripter l'assignation :
{
"name": "update_product",
"arguments": { "platform": "native", "product_id": 12, "patch": { "category": "Compléments" } }
}
…sur les 12 produits en boucle (ton agent MCP peut chaîner les 12 appels Tier-2 successifs).
2. Re-catégoriser à la volée
Tu décides que "Suppléments cognitifs" devient "Brain boosters" pour la version anglaise du site. Plus simple : tu mets à jour les 5 produits via 5 patches MCP, refresh /catalog, la pill change. Aucune migration SQL.
3. Catégorie vide masquée
Si tu marques tous les produits de la catégorie "Boissons" en status='draft', la pill "Boissons" disparaît automatiquement de /catalog. La requête qui calcule les catégories ne ramène que celles ayant au moins un produit actif :
SELECT category, COUNT(*) AS n
FROM native_products
WHERE deleted_at IS NULL
AND status = 'active'
AND category <> ''
GROUP BY LOWER(category), category
ORDER BY n DESC;
Pas de pill fantôme.
Erreurs courantes
- "Ma pill n'apparaît pas" — le produit est
draft, oudeleted_atest setté, oucategoryest vide string. La pill apparaît uniquement quand un produit actif y est rangé. - "Deux pills pour la même catégorie" — un produit a
"Boissons", un autre"boissons". L'index est case-insensitive mais le GROUP BY garde la casse d'origine du premier produit. Édite-en un pour aligner la casse (un seul autocomplete dans l'UI propose la version existante — tu n'as pas dû passer par l'auto-complete). - "Le filter
?category=Xne marche pas" — l'URL est encodée. "Suppléments cognitifs" devient?category=Supplements%20cognitifs. C'est fait automatiquement par l'UI. Si tu testes à la main, n'oublie pas l'encodage. - "Je veux multi-catégoriser" — pas possible nativement. Trois options : (a) duppliquer le produit (déconseillé — duplique les stats), (b) basculer sur un product Shopify et utiliser les collections, (c) hacker via un champ
metadata.categories(JSONB) mais l'UI catalog ne le lira pas. - "L'auto-complete ne propose rien" — l'auto-complete est alimenté par le distinct des catégories existantes. Sur un catalogue vide, tu pars de zéro.
Voir aussi
- products.md — la fiche produit où vit le champ
category. - Landings → AI builder — la catégorie alimente le contexte AI quand tu génères une landing.
- Admin UI → Domains —
root_behavior='catalog'pour servir/catalogà la racine d'un domaine. - MCP
list_collections,get_collection_detail— pour la partie Shopify.