Retour au portfolio
Portfolio · Product Decisions

Les décisions produit
derrière le site

Chaque choix structurant du site — PM Toolkit, KRL1, architecture statique, PLG, observabilité — avec les alternatives rejetées et pourquoi elles ne tiennent pas.

UX PLG Contraintes Arbitrages $0 Budget
13 décisions produit
Décision 01
Pourquoi un assistant IA sur un portfolio PM ?
PLG par le produit, pas par le CV
Un portfolio PM classique, c'est un PDF ou une page LinkedIn. Pour démontrer des compétences AI PM, le portfolio doit être lui-même un produit AI. KRL1 est la preuve concrète : un PM qui comprend les LLMs ne le dit pas, il le montre. Quand un recruteur interagit avec KRL1, il n'a pas besoin de lire "je comprends les systèmes IA" dans le résumé. Il l'expérimente directement. C'est du Product-Led Growth appliqué à la recherche d'emploi : le produit porte son propre discours commercial. KRL1 qualifie aussi les visiteurs naturellement : quelqu'un qui passe 5 minutes à explorer les outils PM via KRL1 est plus engagé qu'un scanner de CV de 10 secondes. Le message LinkedIn "j'ai testé KRL1 et..." est un signal d'engagement fort. L'assistant existe pour le recruteur, pas pour moi.
Alternatives rejetées
  • Chatbot tiers (Crisp, Intercom) — branding générique, aucune valeur différenciante
  • FAQ statique — passive, ne démontre aucune compétence technique
  • Formulaire de contact classique — zéro engagement, zéro preuve
Décision 02
Pourquoi 6 outils PM et pas plus ?
Scope = workflow complet, pas catalogue
6 outils n'est pas un chiffre arbitraire. Il mappe exactement le workflow PM du quotidien : Discovery → User Interviews → OKRs → Backlog → Stories → Roadmap. Pas un outil de plus, pas un de moins. La contrainte vient d'abord du PM Journey : chaque outil devait avoir un "next step" naturel pour que la séquence guidée fonctionne end-to-end. Un 7ème outil aurait créé un dead end ou une branche sans issue. Le scope est aussi dicté par la maintenance : 6 system prompts, 6 pages HTML, 6 entrées pm-session.js. Au-delà, la complexité croît non-linéairement. La contrainte principale a été sémantique : chaque outil devait répondre à une friction réelle vécue en tant que PM, pas à une catégorie théorique. OKR Builder vient de réunions de planning où les KRs étaient flous. Discovery Assistant vient de preps d'interviews mal structurées. 6 frictions réelles = 6 outils.
Alternatives rejetées
  • 10+ outils — maintenance impossible, PM Journey trop complexe
  • 3 outils — scope trop étroit, workflow incomplet
  • Outils génériques (résumé réunion, email writer) — pas différenciants pour un PM
Décision 03
KB fast-path avant le LLM — pourquoi ne pas tout envoyer ?
Coût, vitesse, cohérence sans API
Envoyer chaque message à Groq aurait ajouté ~300-500ms de latence réseau sur 30 à 40% des questions récurrentes ("qui es-tu", "tes outils", "contact"). La KB fast-path intercepte ces patterns localement, répond en moins de 10ms depuis le navigateur, et réserve le quota LLM pour les questions vraiment ouvertes. C'est aussi un guardrail de qualité : les réponses sur le profil de Carlin, les certifications et les outils PM sont précises et stables depuis la KB, sans risque que le LLM reformule légèrement un fait biographique de façon imprécise. 17 règles par langue couvrent aujourd'hui les questions générales, les sujets techniques (RAG, Langfuse, SSE, orchestration, Groq, routes Worker) et la page Product Decisions. Résultat visible : KRL1 semble plus réactif que les chatbots classiques qui prennent toujours 1-2s même pour une question simple.
Alternatives rejetées
  • Tout envoyer au LLM — latence systématique + risque d'imprécision sur les faits
  • Cache LLM — répond pour les questions identiques, pas les variations sémantiques
  • Base de données externe — overhead inutile, la KB tient en mémoire dans le widget
Décision 04
Auto-ouverture KRL1 à 6 secondes — pourquoi ce délai ?
Engagement sans friction
Le timing de l'auto-ouverture est un arbitrage entre engagement et friction. En-dessous de 2 secondes : le visiteur n'a pas encore lu le titre, la popup paraît intrusive et est immédiatement refermée. Au-delà de 15 secondes : la majorité des visiteurs ont déjà formé leur opinion, l'opportunité d'engagement est passée. 6 secondes correspond approximativement au temps de lecture du hero à vitesse normale, titre, sous-titre, tags de stack, premier coup d'oeil aux outils. À ce moment, le visiteur peut recevoir KRL1 comme une aide contextuelle plutôt qu'une interruption. La logique est calquée sur les patterns mesurés des live chats SaaS B2B qui ont identifié ce window optimal. L'auto-ouverture ne se déclenche que si le widget n'a pas été ouvert manuellement entre-temps, pour ne pas interrompre un visiteur déjà engagé.
Alternatives rejetées
  • Pas d'auto-ouverture — KRL1 invisible pour les visiteurs passifs qui ne cliquent pas
  • 2-3 secondes — trop intrusif, perçu comme spam, refusé immédiatement
  • Sur scroll (50%) — trop tardif pour les visiteurs qui rebondissent vite
Décision 05
PM Journey CTAs injectés dans les résultats — pourquoi ?
PLG nudge zéro-friction
Le PM Journey (Discovery → Interviews → OKRs → Backlog → Stories → Roadmap) est la valeur différenciante du toolkit. Sans guidage actif, un PM qui utilise le Discovery Assistant finit sa session et quitte, sans jamais savoir que le User Interview Analyzer est l'étape logique suivante. Le CTA injecté résout ce problème sans modifier 6 pages séparément. KRL1 observe le div #results via MutationObserver depuis krl1-widget.js. Dès que la class .show apparaît (résultat généré), KRL1 injecte automatiquement le CTA "prochaine étape". C'est du PLG pur : la progression dans le workflow est guidée par le produit, pas par une documentation externe. Le couplage est nul : les pages outils n'ont aucune connaissance de cette injection. L'injection est conditionnelle et non-répétitive, elle ne s'affiche qu'une fois par session de résultat.
Alternatives rejetées
  • Liens statiques dans le footer des pages — ignorés par les visiteurs engagés avec le résultat
  • Navigation dans le header — visible mais non-pertinente pendant la génération
  • Email de récapitulatif — hors portée, pas de compte utilisateur, friction inutile
Décision 06
Streaming SSE sur la synthesis — pourquoi pas une réponse bloquante ?
Latence perçue vs latence réelle
Sans streaming, une requête KRL1 (planner ~600ms + synthesis ~900ms) implique ~1.5 à 2.5 secondes de spinner sans feedback. Avec le streaming SSE, les premiers tokens de la synthesis s'affichent en ~400ms pendant que la génération continue. La latence perçue est divisée par trois environ, sans que la latence réelle change. Même principe que le skeleton loading en UI : réduire l'attente perçue plutôt que l'attente réelle. Le rendu mot par mot avec un délai de 22ms entre chaque mot ajoute un effet "typewriter" subtil qui renforce la sensation d'intelligence. Le streaming impose que le planner (appel LLM n°1) reste toujours bloquant car son output JSON est nécessaire avant de lancer la synthesis. Seul l'appel n°2 streame. En cas d'échec, le widget bascule automatiquement sur /orchestrate avec zéro interruption visible.
Alternatives rejetées
  • Réponse bloquante — ~2.5s de spinner, UX 2015, taux d'abandon élevé
  • Réponse plus courte pour aller plus vite — sacrifie la qualité de la réponse
  • Skeleton placeholder avec contenu fictif — complexe à implémenter, faux feedback
Décision 07
Limite 220 mots sur les réponses KRL1 — pourquoi ?
Densité > exhaustivité
220 mots est une contrainte de design délibérée, pas une limitation technique. Sans contrainte, un LLM produit des réponses essay de 500 à 800 mots : pédagogiques, correctes, mais inadaptées au contexte d'un chat de portfolio. Un recruteur cherche une réponse dense et directe, avec un prochain pas concret. Il ne veut pas lire un article. La limite oblige le modèle à prioriser l'essentiel : l'intent de l'utilisateur, la recommandation claire, le lien vers l'outil approprié. C'est aussi une contrainte économique : max_tokens 450 garde chaque synthesis dans un range de coût prévisible. Le paramètre temperature 0.45 pour la synthesis est calibré pour que la réponse reste naturelle et non-robotique dans ce format court. En pratique, la plupart des réponses font 150 à 200 mots.
Alternatives rejetées
  • Pas de limite — réponses essay, noie l'essentiel, perd l'utilisateur
  • 100 mots — trop court pour les sujets PM complexes, répond en surface
  • Limite dynamique selon l'intent — surengineering pour un gain marginal
Décision 08
Budget $0 — pourquoi seulement des free tiers ?
Contrainte = créativité technique
La contrainte $0 n'est pas une restriction : c'est un parti pris de conception. Un portfolio qui démontre des compétences AI PM doit montrer qu'on peut construire quelque chose de qualité dans les contraintes réelles d'un side project. Payer pour des serveurs ou des APIs premium aurait été la solution facile. Choisir Groq, Cloudflare Workers, GitHub Pages, Langfuse Cloud, Workers AI, tous sur free tier, a forcé des décisions architecturales créatives. Le RAG edge-native dans le Worker existe parce que le free tier Render était trop limité pour Python. Cette contrainte a produit une architecture plus propre qu'un budget illimité ne l'aurait fait. Pour un recruteur AI PM, un système fonctionnel construit à $0/mois est plus impressionnant qu'une solution identique à $200/mois. Le $0 est un signal de resourcefulness et de pensée contrainte, deux qualités centrales d'un bon PM.
Alternatives rejetées
  • Render + Railway payant — solution fonctionnelle mais banale, aucune ingéniosité démontrée
  • AWS/GCP — $20-50/mois, over-engineered pour un portfolio personnel
  • Vercel + Supabase — bon stack mais coûts dès que le free tier est dépassé
Décision 09
Langfuse en production sur un portfolio — pourquoi ?
Build → mesure → décide
Mettre de l'observabilité LLM sur un portfolio personnel peut paraître over-engineered. Ce ne l'est pas. Langfuse permet de fermer la boucle build → mesure → décide, le cycle que je défends comme PM. Sans données de production, impossible de savoir : quels intents sont les plus fréquents, où la confidence du planner est faible (en-dessous de 0.6), quels outils KRL1 recommande le plus, combien de tokens sont consommés par route. Ces métriques permettent d'itérer sur le system prompt, les règles KB, le flow d'orchestration. La décision d'intégrer Langfuse via l'API REST directement dans le Worker (sans SDK) vient de la contrainte zero-npm. Le flush via ctx.waitUntil garantit zéro latence perçue ajoutée. Pour le portfolio, Langfuse démontre concrètement qu'on pense "mesure" avant même d'avoir des vrais utilisateurs.
Alternatives rejetées
  • Console.log uniquement — pas de données structurées, pas d'historique, pas d'analyse
  • LangSmith — lié à LangChain, inadapté à une architecture Worker JS pure
  • Pas d'observabilité — aucune feedback loop, impossible d'itérer informés
Décision 10
Bilingue FR/EN dès le départ — pourquoi pas ajouter EN plus tard ?
Audience recruteur internationale
La décision de supporter FR et EN dès le premier jour vient d'une expérience produit directe : ajouter l'i18n en retard sur une base existante est douloureux et crée de la dette technique. Partir bilingue impose une discipline de code : chaque string de UI passe par un objet de traductions, zéro text hardcodé dans le HTML. Le surcoût initial est d'environ 20% de temps par page. Le bénéfice : aucun refactoring i18n à prévoir, et surtout une audience élargie. Les recruteurs tech opèrent souvent dans des contextes multilingues. KRL1 détecte la langue via localStorage et URL param (?lang=en), ce qui permet de partager des liens dans une langue spécifique. Le choix de l'i18n vanilla (objet T + setLang()) évite toute dépendance externe pour seulement deux langues.
Alternatives rejetées
  • FR uniquement — audience limitée à la France, signale peu d'ambition internationale
  • Ajouter EN plus tard — dette technique assurée, refactoring douloureux
  • Bibliothèque i18n (i18next) — dépendance inutile pour seulement deux langues
Décision 11
Pourquoi un hub PM Toolkit plutôt que 6 liens séparés sur le portfolio ?
Chaîner les outils = refléter le vrai workflow PM
Un PM n'utilise jamais un outil de façon isolée. Il enchaîne : discovery → interviews → OKRs → backlog → stories → roadmap. Garder les 6 outils comme des cartes indépendantes forçait l'utilisateur à recoller le contexte à la main d'une session à l'autre. Le hub pm-toolkit.html résout deux problèmes : il rend la séquence lisible et il centralise l'entrée dans le workflow. Le PM Journey CTA (injecté par KRL1 via MutationObserver) suggère automatiquement l'étape suivante après chaque résultat généré. C'est du PLG au niveau du design d'information : le produit guide naturellement le workflow, sans documentation externe, sans friction. Pour le recruteur, naviguer dans le PM Toolkit est une démonstration concrète de pensée workflow — il voit non pas 6 features isolées, mais un produit cohérent avec une logique de bout en bout.
Alternatives rejetées
  • Liens séparés sur le portfolio — chaque outil vécu comme une feature isolée, pas de séquence visible
  • Navigation header avec 6 entrées — cognitive overload, pas de guidage contextuel
  • Workflow manuel (copier-coller) — friction maximale, perte de contexte garantie
Décision 12
Pourquoi localStorage pour passer le contexte entre outils PM ?
Continuité sans backend, sans compte, sans friction
Le problème : comment transférer un insight d'un outil PM au suivant sans que l'utilisateur ait à tout recopier. Les solutions naturelles — base de données, sessions serveur — nécessitent un backend et un compte utilisateur. Avec localStorage et un module léger (pm-session.js), chaque outil sauvegarde ses sorties structurées (problème cadré, persona validé, OKRs générés, top-priorité RICE...) sous des clés communes. L'outil suivant peut les importer en un clic, avec un feedback visuel (bordure colorée) pour signaler qu'un contexte est disponible. Pas de compte, pas de serveur, pas de RGPD à gérer. La contrainte $0 et l'architecture HTML statique ont rendu cette solution non seulement acceptable mais élégante — une contrainte qui a forcé la simplicité plutôt que de la contraindre.
Alternatives rejetées
  • Base de données + API — nécessite un backend, un compte, de la gestion de sessions
  • URL params — taille limitée, données sensibles potentiellement exposées
  • Copier-coller manuel — friction maximale, erreur de saisie garantie
Décision 13
Pourquoi HTML/JS vanilla sans framework ?
Zéro dépendance, zéro dette, déploiement immédiat
Le choix de ne pas utiliser React, Vue ou Next.js n'est pas une limitation — c'est une décision explicite. Pour un portfolio de 6 outils PM et quelques pages de documentation, un framework SPA aurait apporté : une étape de build obligatoire, des dépendances npm à maintenir, une complexité d'abstraction sans valeur ajoutée. Chaque page HTML est autonome avec son CSS et son JS inline. Pas de routing, pas de state management global. Le déploiement est un git push qui déclenche GitHub Pages en moins de 60 secondes. Le debug se fait directement dans les DevTools sans source maps. La lisibilité du code est maximale : pas de JSX, pas de hooks, pas de Webpack à comprendre avant de contribuer. Pour un PM qui vibe code en spare time avec Claude Code, la vélocité prime sur l'architecture. Le stack vanilla démontre aussi qu'on peut livrer un produit IA fonctionnel sans dépendre d'un framework.
Alternatives rejetées
  • React / Next.js — étape de build, config webpack/eslint, overhead pour du contenu statique
  • Vue.js — même problème, complexité non justifiée pour ce cas d'usage
  • Astro — plus adapté, mais paradigme à apprendre, overhead de config initial
Décision 14
Pourquoi remplacer l'accordéon par des tabs ?
Navigation immédiate, zéro friction d'exploration
L'accordéon avait un défaut fondamental : le contenu était invisible par défaut. Le visiteur devait deviner qu'il y avait quelque chose à déplier, cliquer, puis scroller pour évaluer si ça valait le coup. Les tabs inversent ce rapport : le libellé de l'onglet suffit à savoir ce qu'on va trouver, le switch est instantané, sans reflow ni animation de dépliage. La navigation contextuelle (liens nav qui apparaissent en stagger au scroll via IntersectionObserver) remplace l'accordéon comme affordance de navigation sans dupliquer la barre principale.
Alternatives rejetées
  • Accordéon — contenu caché par défaut, friction au clic, scroll non prévisible après ouverture
  • Sections toujours visibles — page trop longue, pas de focus, difficile à parcourir rapidement
  • Bento grid cliquable — UX non standard pour du contenu dense et textuel
Décision 15
Pourquoi charger Architecture et Décisions produit en lazy-load ?
Index léger, pages autonomes préservées, styles isolés
Intégrer les pages Architecture (1 779 lignes) et Décisions produit (500 lignes) directement dans index.html aurait créé un fichier monolithique dépassant 4 000 lignes, avec des conflits CSS garantis entre les systèmes de styles de chaque page. Le lazy-load (fetch + DOMParser) charge le contenu à la demande, au premier clic seulement. Chaque page reste une URL autonome et partageabl. Le CSS injecté est scopé à l'ID du tab panel (#architecture .decision-card) pour confiner son effet — impossible d'écraser les glows du hero ou les styles des autres sections. Le guard dataset.loaded évite les doubles requêtes si on revient sur un tab déjà chargé.
Alternatives rejetées
  • Intégration statique — index.html > 4 000 lignes, conflits CSS, maintenance difficile
  • iFrame — pas de thème dark/light partagé, barre de scroll parasite, UX dégradée
  • Pages séparées uniquement — navigation fragmentée, on quitte l'index à chaque clic