<?xml version="1.0" encoding="utf-8"?>
<rss version="2.0" xmlns:dc="http://purl.org/dc/elements/1.1/" xmlns:content="http://purl.org/rss/1.0/modules/content/" xmlns:atom="http://www.w3.org/2005/Atom">
    <channel>
        <title>Premier Octet</title>
        <link>https://www.premieroctet.com</link>
        <description>Premier Octet est une agence de développement d'applications Web, mobiles et IA basée à Paris, experte en React</description>
        <lastBuildDate>Fri, 06 Feb 2026 17:10:52 GMT</lastBuildDate>
        <docs>https://validator.w3.org/feed/docs/rss2.html</docs>
        <generator>https://github.com/jpmonette/feed</generator>
        <language>fr</language>
        <image>
            <title>Premier Octet</title>
            <url>https://www.premieroctet.com/images/logo-600x600.png</url>
            <link>https://www.premieroctet.com</link>
        </image>
        <copyright>All rights reserved 2026, Premier Octet</copyright>
        <atom:link href="https://www.premieroctet.com/atom.xml" rel="self" type="application/rss+xml"/>
        <item>
            <title><![CDATA[Guide pratique du Model Context Protocol : tout comprendre sur les tools et serveurs MCP]]></title>
            <link>https://www.premieroctet.com/blog/guide-pratique-du-model-context-protocol-tout-comprendre-sur-les-tools-et-serveurs-mcp</link>
            <guid>https://www.premieroctet.com/blog/guide-pratique-du-model-context-protocol-tout-comprendre-sur-les-tools-et-serveurs-mcp</guid>
            <pubDate>Wed, 29 Oct 2025 00:00:00 GMT</pubDate>
            <description><![CDATA[Comprenez les tools, la différence avec les serveurs MCP, les transports stdio et HTTP, et comment choisir selon votre contexte (SaaS, desktop, agents IA).]]></description>
            <content:encoded><![CDATA[<p>Depuis presque un an maintenant, <a href="https://www.anthropic.com/">Anthropic</a> a initié un protocole de communication permettant de fournir des informations contextuelles aux modèles et a donné naissance au <a href="https://modelcontextprotocol.io/docs/getting-started/intro"><strong>Model Context Protocol (MCP)</strong></a>. Il est devenu, au fil du temps, un standard dans l&#x27;écosystème IA pour les applications qui souhaitent utiliser des modèles IA de manière plus efficace et les personnes qui souhaitent augmenter les capacités de leurs modèles IA.</p>
<p>Cependant, la diffusion progressive de ce protocole, essentielle à son adoption, s&#x27;accompagne aussi d&#x27;un certain nombre de questions et de confusions. C&#x27;est ce que nous allons tenter de clarifier dans cet article en détaillant les différents cas d&#x27;utilisation possibles et les meilleures pratiques à appliquer en fonction de vos besoins.</p>
<p><strong>À quoi sert le protocole MCP ?</strong> <strong>Quels sont les cas d&#x27;utilisation possibles ?</strong> <strong>Comment utiliser ce protocole dans vos applications ?</strong> <strong>Quelles sont les limites de ce protocole ?</strong> <strong>À quels besoins répond ce protocole ?</strong></p>
<h2>Introduction</h2>
<p>Force est de constater que l&#x27;utilisation des <strong>Large Language Models (LLMs)</strong> augmente de manière significative dans tous les domaines, allant de la simple utilisation par le grand public de chatbots IA comme ChatGPT (<a href="https://gs.statcounter.com/ai-chatbot-market-share">en position de quasi-monopole aujourd&#x27;hui</a>) à l&#x27;utilisation sur-mesure dans des applications complexes, en passant par des assistants de développement comme Cursor, Zed, etc.</p>
<p>Mais ces outils puissants souffraient de deux problèmes majeurs:</p>
<ul>
<li><strong>le manque de contexte</strong> (ex: données de la base de données, documents, etc.)</li>
<li><strong>l&#x27;impossibilité d&#x27;effectuer des actions</strong> (ex: envoyer un email, modifier un document, créer un événement, etc.)</li>
</ul>
<p>Pour pallier au premier problème de contexte, des solutions ont été mises en place comme le <strong>Retrieval-Augmented Generation (RAG)</strong>, que l&#x27;on peut voir comme une sorte de bibliothèque permettant aux LLMs d&#x27;accéder à des informations contextuelles, et le <strong>fine-tuning</strong> de modèles pour permettre aux LLMs d&#x27;être spécialisés dans un domaine précis.
Ces sujets ne seront pas abordés dans cet article, mais si le fonctionnement de ces solutions vous intéresse, vous pouvez consulter <a href="https://www.premieroctet.com/blog/comment-fonctionne-un-rag">Comment fonctionne un RAG ?</a>.</p>
<div type="info"><p>À noter qu&#x27;il n&#x27;existe pas de standard pour les RAG. Chaque implémentation doit être adaptée à la
plateforme utilisée.</p></div>
<p>Enfin, pour pallier l&#x27;impossibilité d&#x27;effectuer des actions, on a vu apparaître la notion de <strong>tools</strong> (ou <strong>outils</strong> en français).</p>
<h2>Qu&#x27;est-ce qu&#x27;un tool ?</h2>
<p>Un <code>tool</code> est une fonctionnalité qui peut être appelée par un LLM pour effectuer une tâche spécifique. Par exemple, un tool pourrait être utilisé pour envoyer un email, modifier un document, créer un événement, etc. L&#x27;appel d&#x27;un <code>tool</code> permet donc d&#x27;exécuter une fonction spécifique sur un serveur et de retourner un résultat qui sera soit affiché dans la conversation, soit utilisé pour donner du contexte à la requête.</p>
<p><img src="https://www.premieroctet.com/blog/guide-pratique-du-model-context-protocol-tout-comprendre-sur-les-tools-et-serveurs-mcp/tools.svg" alt="Tool Schema"/></p>
<ol>
<li>L&#x27;utilisateur fait une demande au LLM</li>
<li>Le LLM analyse la demande et détermine s&#x27;il existe un outil qui pourrait répondre à la demande</li>
<li>Si non : Le LLM génère directement la réponse</li>
<li>Si oui : Le LLM invoque l&#x27;outil</li>
<li>Le serveur qui implémente le tool exécute l&#x27;action demandée par le tool</li>
<li>Le serveur retourne le résultat à l&#x27;utilisateur ou au LLM qui peut alors formuler une réponse complète à l&#x27;utilisateur</li>
</ol>
<div type="info"><p>Ce schéma est une simplification pour illustrer le fonctionnement d&#x27;un tool. En réalité, un tool
peut être composé de plusieurs étapes et de plusieurs types de retours voire même réinterprété par
le LLM.</p></div>
<h3>Où s&#x27;exécute un tool ?</h3>
<p>Comme on peut le voir sur le schéma ci-dessus, un <code>tool</code> est appelé par un LLM et exécuté sur le serveur qui l&#x27;a implémenté. C&#x27;est ce serveur qui va exécuter la fonctionnalité spécifique et retourner le résultat.</p>
<p>C&#x27;est pour cette raison que les <code>tools</code> &quot;classiques&quot; ne peuvent pas être exécutés directement par un agent distant, par &quot;distant&quot; on entend un agent qui n&#x27;est pas hébergé sur le même serveur que le serveur qui implémente le tool ou dans un processus différent. Ils sont donc très utiles pour les applications qui implémentent leur propre agent IA.</p>
<h2>Qu&#x27;est-ce que le protocole MCP ?</h2>
<p>On l&#x27;a vu précédemment, l&#x27;infrastructure des <code>tools</code> est très simple, mais elle souffre du fait que leur exécution est limitée à leur propre environnement et ils ne peuvent être appelés et exécutés que dans celui-ci.</p>
<p>Pour pouvoir exposer des tools (entre autres) à un agent IA, on a vu apparaître le protocole <a href="https://modelcontextprotocol.io/">Model Context Protocol (MCP)</a>. Il s&#x27;agit d&#x27;un protocole de communication qui permet de faire le pont entre des agents IA et des tools présents sur un serveur MCP.</p>
<p>Ce protocole repose sur deux composantes principales:</p>
<ul>
<li>Le <strong>serveur MCP</strong> qui implémente et expose les <code>tools</code> disponibles</li>
<li>Le <strong>client MCP</strong> qui appelle les <code>tools</code> disponibles et reçoit les résultats</li>
</ul>
<div type="info"><p>Les serveurs MCP peuvent aussi exposer des <code>resources</code> et des <code>prompts</code> en plus des <code>tools</code>.
Cependant, beaucoup d&#x27;hôtes MCP distants ne supportent pas ces fonctionnalités.</p></div>
<p>La communication entre ces deux entités est standardisée via un format <a href="https://www.jsonrpc.org/">JSON-RPC</a> qui permet de faire transiter des ordres d&#x27;instructions (actions/procédures) entre un client et un serveur.</p>
<p>Le fait d&#x27;avoir deux entités distinctes nous donne la flexibilité d&#x27;utiliser le protocole MCP dans différents contextes et d&#x27;avoir nos <code>tools</code> accessibles depuis des environnements différents.</p>
<h2>Les hôtes MCP</h2>
<p>Dans la suite de cet article, nous allons parler de <strong>hôte MCP</strong> pour désigner l&#x27;entité qui implémente à la fois la communication avec un LLM et un client MCP. On pourra ainsi faire la distinction entre un <strong>hôte MCP distant</strong> et un <strong>hôte MCP local</strong>. Il est important de les différencier car ils n&#x27;auront pas les mêmes contraintes et possibilités.</p>
<h3>Hôte MCP distant</h3>
<p>Un hôte MCP distant ou <code>remote</code> est souvent un service web qui propose un chatbot IA comme ChatGPT ou Claude AI (version web). Pour savoir si un agent IA supporte le protocole MCP, il faut vérifier ses capacités dans l&#x27;interface web de l&#x27;agent IA et vérifier s&#x27;il est fait mention de <code>Connected apps</code> ou <code>Connecteurs</code>.</p>
<div type="info"><p>Même si le protocole MCP devient un standard dans l&#x27;écosystème IA, il n&#x27;est pas encore supporté
par tous les services web qui proposent un chatbot IA.</p></div>
<h3>Hôte MCP local</h3>
<p>Un hôte MCP local est une entité qui a pour environnement votre propre machine. Il tourne sur votre propre machine et peut donc effectuer des actions dessus. Dans cette catégorie, on peut citer les assistants de développement comme Cursor, Zed, etc., mais aussi des versions desktop d&#x27;agents conversationnels comme <a href="https://claude.com/download">Claude Desktop</a> par exemple.</p>
<h2>Les différents types de transports</h2>
<p>Il existe différents types de transports pour la communication entre le client et le serveur MCP:</p>
<ul>
<li><a href="https://modelcontextprotocol.io/specification/2025-06-18/basic/transports#stdio">Stdio</a></li>
<li><a href="https://modelcontextprotocol.io/specification/2025-06-18/basic/transports#streamable-http">Streamable HTTP</a></li>
</ul>
<p>Le choix du transport va dépendre des besoins et des contraintes de l&#x27;application.</p>
<h3>Stdio</h3>
<p>Le transport <code>stdio</code> est un transport qui utilise le standard input/output (stdin/stdout) pour la communication entre le client et le serveur. Avec ce mode de transport, tout se passe en local, c&#x27;est-à-dire que le client et le serveur sont hébergés localement et le serveur peut effectuer des actions sur la machine locale comme lire des fichiers, écrire des fichiers, etc.</p>
<p>Il va de pair avec un hôte MCP local. Les hôtes MCP distants n&#x27;utilisent pas ce type de transport.</p>
<p>Ce transport est idéal pour les assistants de développement et les applications desktop qui ont besoin d&#x27;accéder aux ressources locales de l&#x27;utilisateur.</p>
<p><img src="https://www.premieroctet.com/blog/guide-pratique-du-model-context-protocol-tout-comprendre-sur-les-tools-et-serveurs-mcp/stdio.svg" alt="Stdio Transport"/></p>
<p>Le serveur MCP est lancé en local en tant que sous-processus de l&#x27;hôte MCP local.</p>
<div type="warning"><p>Le transport <code>stdio</code> expose des API natives de l&#x27;hôte MCP local. Il est donc important de
s&#x27;assurer que l&#x27;hôte MCP local provient d&#x27;une source fiable et de confiance.</p></div>
<p>Si l&#x27;implémentation d&#x27;un serveur MCP en transport <code>stdio</code> vous intéresse, vous pouvez consulter <a href="https://www.premieroctet.com/blog/integration-mcp-dans-une-app-react">Intégration d&#x27;un MCP dans une application React</a>.</p>
<h3>Streamable HTTP</h3>
<p>Le transport <code>streamable HTTP</code> est un transport qui utilise le protocole HTTP pour la communication entre le client MCP et le serveur MCP. Il nous permet de découpler l&#x27;hôte et le serveur et de les héberger sur des serveurs différents, permettant ainsi d&#x27;utiliser des serveurs MCP avec des hôtes MCP distants et locaux.</p>
<p>Le serveur MCP doit être déployé sur un serveur web et accessible via une URL. L&#x27;hôte pourra ainsi appeler les tools du serveur MCP via une requête HTTP. Le transport <code>streamable HTTP</code> est idéal pour les hôtes MCP distants.</p>
<p><img src="https://www.premieroctet.com/blog/guide-pratique-du-model-context-protocol-tout-comprendre-sur-les-tools-et-serveurs-mcp/streamable-http.svg" alt="Streamable HTTP Transport"/></p>
<p>Dans le cadre d&#x27;un SaaS par exemple, on pourra héberger le serveur MCP sur un serveur web ou créer un endpoint <code>/mcp</code> dans notre API, et un utilisateur aura la possibilité d&#x27;ajouter un connecteur à son hôte MCP distant, par exemple Claude AI ou ChatGPT, pour utiliser nos <code>tools</code>.</p>
<h4>Sécurité</h4>
<p>Dans certains cas, on peut avoir besoin d&#x27;authentifier ou d&#x27;autoriser l&#x27;utilisation des outils exposés. Dans ce cas, on peut utiliser un token d&#x27;authentification ou une clé API pour sécuriser l&#x27;accès au serveur MCP. L&#x27;utilisation de ce transport nous permet donc de sécuriser l&#x27;accès aux outils exposés par le serveur MCP avec une authentification basique ou plus avancée avec <a href="https://modelcontextprotocol.io/specification/2025-06-18/basic/authorization">OAuth 2</a>.</p>
<h2>Comment utiliser le protocole MCP dans vos applications ?</h2>
<p>Tout d&#x27;abord, avant de commencer à utiliser le protocole MCP dans vos applications, il est important de comprendre comment il fonctionne et quelles sont les différentes possibilités offertes par le protocole. Ensuite, il faut définir le besoin de votre application et choisir le transport qui correspond le mieux à vos besoins.</p>
<p><strong>Si votre application est un SaaS</strong>, alors vous souhaitez probablement exposer vos <code>tools</code> au grand public. Dans ce cas, vous aurez besoin d&#x27;utiliser le transport <code>streamable HTTP</code> afin de permettre aux utilisateurs de connecter leur hôte MCP distant, par exemple Claude AI ou ChatGPT, à votre serveur MCP.</p>
<blockquote>
<p>Les utilisateurs d&#x27;hôtes locaux pourront aussi utiliser votre solution avec ce mode de transport.</p>
</blockquote>
<p><strong>Si votre solution est un script ou une application autonome</strong>, alors vous aurez besoin d&#x27;utiliser le transport <code>stdio</code> afin de permettre aux utilisateurs de connecter leur hôte MCP local à votre serveur MCP.</p>
<p><strong>Si votre solution a un usage exclusivement interne</strong>, alors vous n&#x27;avez probablement pas besoin d&#x27;utiliser le protocole MCP. Exception faite si vous souhaitez utiliser des <code>tools</code> sur plusieurs agents IA internes. Dans ce cas, il faudra utiliser le transport <code>stdio</code> pour permettre aux agents IA de communiquer avec le serveur MCP (si celui-ci est sur le même serveur).</p>
<h2>Adhésion au protocole</h2>
<p>L&#x27;efficacité d&#x27;un MCP est directement liée à l&#x27;adhésion au protocole par les hôtes MCP distants. En effet, si un hôte MCP distant ne supporte pas le protocole MCP, alors le client MCP ne pourra pas utiliser les <code>tools</code> exposés par le serveur MCP. À ce jour, de plus en plus d&#x27;hôtes MCP distants supportent le protocole MCP, mais la plupart du temps l&#x27;ajout de connecteurs fait partie de fonctionnalités payantes, c&#x27;est le cas de ChatGPT et Claude AI par exemple. Ce qui freine donc la démocratisation de l&#x27;utilisation du protocole MCP par le grand public.</p>
<h2>Registre</h2>
<p>Il existe un registre officiel des serveurs MCP disponibles. Il est disponible sur le site de <a href="https://github.com/modelcontextprotocol/servers">Model Context Protocol</a> et permet de trouver les serveurs MCP disponibles et de les utiliser dans vos agents IA.</p>
<p>Si la mise en place de serveurs MCP par les entreprises continue de se démocratiser, il faudra s&#x27;attendre à ce que d&#x27;autres registres voient le jour pour permettre aux utilisateurs de trouver les serveurs MCP à la manière des moteurs de recherche traditionnels. Si c&#x27;est le cas, le référencement auprès de ces registres pourra devenir un enjeu majeur pour les entreprises.</p>
<h2>Conclusion</h2>
<p>Vous l&#x27;aurez compris, le protocole MCP est un standard qui va changer la donne dans l&#x27;écosystème IA. En permettant aux LLMs d&#x27;accéder à des données contextuelles et d&#x27;effectuer des actions concrètes, il transforme ces modèles de simples générateurs de texte en véritables assistants capables d&#x27;agir dans le monde réel.</p>
<p>Le choix du transport dépendra de votre contexte : <code>stdio</code> pour les assistants de développement et applications desktop, <code>streamable HTTP</code> pour les solutions SaaS et l&#x27;intégration avec des hôtes distants comme Claude AI ou ChatGPT.</p>
<p>Même si l&#x27;adoption par les hôtes MCP distants est encore limitée et souvent payante, la tendance est clairement à la démocratisation.</p>
<p>Chez Premier Octet, nous suivons de près ces évolutions. Si vous souhaitez explorer cette technologie pour vos besoins ou avez des questions sur l&#x27;implémentation, n&#x27;hésitez pas à <a href="https://www.premieroctet.com/contact">nous contacter</a>.</p>
<h2>Ressources</h2>
<ul>
<li><a href="https://modelcontextprotocol.io/">Model Context Protocol</a></li>
<li><a href="https://github.com/modelcontextprotocol/servers">Serveurs MCP officiels</a></li>
<li><a href="https://www.premieroctet.com/blog/integration-mcp-dans-une-app-react">Intégration d&#x27;un MCP dans une application React</a></li>
<li><a href="https://jolicode.com/blog/mcp-the-open-protocol-that-turns-llm-chatbots-into-intelligent-agents">MCP: The Open Protocol That Turns LLM Chatbots into Intelligent Agents</a></li>
</ul>]]></content:encoded>
            <enclosure url="https://www.premieroctet.com/blog/guide-pratique-du-model-context-protocol-tout-comprendre-sur-les-tools-et-serveurs-mcp/illu.webp" length="0" type="image/webp"/>
        </item>
        <item>
            <title><![CDATA[Better Auth : structure et permissions avec le plugin Organization]]></title>
            <link>https://www.premieroctet.com/blog/better-auth-structure-et-permissions-avec-le-plugin-organization</link>
            <guid>https://www.premieroctet.com/blog/better-auth-structure-et-permissions-avec-le-plugin-organization</guid>
            <pubDate>Wed, 15 Oct 2025 00:00:00 GMT</pubDate>
            <description><![CDATA[Découvrez en profondeur le plugin Organization de Better Auth : gestion du state client/serveur, personnalisation du modèle, permissions avancées et la configuration Teams.]]></description>
            <content:encoded><![CDATA[<p>Après avoir exploré <a href="https://www.premieroctet.com/blog/better-auth-futur-authjs">les bases de <strong>Better Auth</strong></a>, nous allons aujourd&#x27;hui nous intéresser à l’un des plugins que nous avions mentionné : <a href="https://www.better-auth.com/docs/plugins/organization"><strong>Organization</strong></a>.</p>
<p>Gérer une hiérarchie d’utilisateurs et de permissions dans une application peut vite devenir un casse-tête. Le plugin Organization de Better Auth propose une approche élégante et modulaire : il permet de gérer des structures multi-utilisateurs (équipes, entreprises, projets...), d’y associer des rôles et d’implémenter des permissions.</p>
<p>Voici la liste des points que nous allons aborder :</p>
<ul>
<li>la gestion du <strong>state entre client et serveur</strong>,</li>
<li>la <strong>personnalisation du modèle Organization</strong>,</li>
<li>la <strong>mise en place d’un système de permissions avancé</strong>,</li>
<li>et enfin l’activation du mode <strong>Teams</strong>, pour les cas les plus complexes.</li>
</ul>
<h2>Séparation du state entre serveur et client</h2>
<p>Un point de base essentiel à comprendre est que <code>auth</code> (serveur) et <code>authClient</code> (client) fonctionnent comme deux systèmes séparés, ils ne partagent <strong>pas d’état synchronisé automatiquement</strong>.</p>
<h3>👉 En pratique :</h3>
<ul>
<li>Les actions exécutées via <code>auth.api</code> ne mettent pas à jour automatiquement les hooks client (ex. <code>useSession</code>, <code>useActiveOrganization</code>).</li>
<li>Il faut rafraîchir explicitement l’état client après toute action ayant un impact sur la session, l’organisation ou les rôles. Par exemple, le hook <code>useActiveOrganization()</code> propose un <code>refetch()</code> pour rafraîchir les données de l’organisation active côté client.</li>
</ul>
<p>Ce comportement découle de la séparation entre client et serveur : Better Auth ne synchronise pas automatiquement ces deux états. Cela donne au développeur un contrôle total sur le rafraîchissement côté client. Similaire à d’autres librairies d’authentification modernes, <strong>la synchronisation automatique du state n’est généralement pas implémentée pour des raisons de performance et de sécurité.</strong></p>
<h2>Personnaliser le modèle Organization</h2>
<p>Les tables nécessaires au plugin Organization sont personnalisables : il est possible de les <a href="https://www.better-auth.com/docs/plugins/organization#customizing-the-schema">renommer ainsi que leurs champs</a> ou <a href="https://www.better-auth.com/docs/plugins/organization#additional-fields-1">d&#x27;ajouter des champs supplémentaires</a>. Le CLI de Better Auth se chargera ensuite de générer le schéma correspondant dans votre base de données via la commande <code>npx @better-auth/cli generate</code> (ici, nous utilisons <a href="https://www.premieroctet.com/blog/better-auth-futur-authjs#installation-et-gestion-de-la-base-de-donnees">Prisma avec son adapter</a>).</p>
<p>Dans cet exemple, nous allons renommer <code>Organization</code> en <code>Project</code> et son champ <code>name</code> en <code>title</code> (mais dans le reste de l&#x27;article, nous continuerons à parler de <code>organization</code>).</p>
<div><div><div value="tab-1">auth.ts</div><div value="tab-2">schema.prisma.ts</div></div><div value="tab-1"><pre><code class="language-ts">plugins: [
  organization({
    schema: {
      organization: {
        modelName: &#x27;project&#x27;, // `Organization` sera renommé `Project`
        fields: {
          name: &#x27;title&#x27;, // `name` sera renommé `title`
        },
      },
      member: {
        additionalFields: {
          name: {
            // On ajoute un champ `name` à la table `Member`
            type: &#x27;string&#x27;,
            input: true,
            required: false,
          },
        },
      },
    },
  }),
]
</code></pre></div><div value="tab-2"><pre><code class="language-ts">// Voici les tables générées

model Project {
  id          String       @id
  title       String
  slug        String?
  logo        String?
  createdAt   DateTime
  metadata    String?
  members     Member[]
  invitations Invitation[]

  @@unique([slug])
  @@map(&quot;project&quot;)
}

model Member {
  id             String   @id
  organizationId String
  project        Project  @relation(fields: [organizationId], references: [id], onDelete: Cascade)
  userId         String
  user           User     @relation(fields: [userId], references: [id], onDelete: Cascade)
  role           String
  createdAt      DateTime
  name           String?

  @@map(&quot;member&quot;)
}

model Invitation {
  id             String   @id
  organizationId String
  project        Project  @relation(fields: [organizationId], references: [id], onDelete: Cascade)
  email          String
  role           String?
  status         String
  expiresAt      DateTime
  inviterId      String
  user           User     @relation(fields: [inviterId], references: [id], onDelete: Cascade)

  @@map(&quot;invitation&quot;)
}

</code></pre></div></div>
<p>⚠️ Le renommage n’affecte <strong>pas les noms des méthodes SDK</strong> (<code>authClient.organization.*</code>, <code>auth.api.*</code>).</p>
<pre><code class="language-ts">// ✅ Correct
const { data, error } = await authClient.organization.delete({
  organizationId: activeOrganization!.id,
})

// ❌ Incorrect
const { data, error } = await authClient.project.delete({
  projectId: activeProject!.id,
})
</code></pre>
<h2>Mettre en place un système de permissions</h2>
<p><img src="https://www.premieroctet.com/blog/better-auth-structure-et-permissions-avec-le-plugin-organization/manage-roles.jpg" alt="Gestion des rôles : capture de l’UI des permissions"/></p>
<p>C’est sans doute l&#x27;une des parties les plus intéressantes du plugin Organization : Better Auth fournit un système d’<a href="https://www.better-auth.com/docs/plugins/organization#access-control"><strong>Access Control</strong></a> flexible avec son <code>createAccessControl</code>. Il s’appuie sur une approche déclarative de la sécurité, où chaque <strong>rôle</strong> se voit attribuer un ensemble d’<strong>actions autorisées</strong> sur des <strong>entités</strong> précises.</p>
<h3>Résumé des termes :</h3>
<ul>
<li><strong>Entité</strong> → représente un objet sur lequel on agit (<code>organization</code>, <code>member</code>, <code>invitation</code>).</li>
<li><strong>Action</strong> → représente ce qu’on peut faire sur une entité (<code>update</code>, <code>delete</code>, etc.).</li>
<li><strong>Permission</strong> → est la combinaison entité + action (un <code>member</code> avec l&#x27;action <code>create</code> sur l&#x27;entité <code>invitation</code> peut créer une invitation).</li>
<li><strong>Rôle</strong> → est un ensemble de permissions accordées à un utilisateur.</li>
</ul>
<h3>Exemple de fichier de configuration :</h3>
<p>Voici un exemple minimal pour déclarer des actions par entité et composer des rôles.</p>
<pre><code class="language-ts">import { createAccessControl } from &#x27;better-auth/plugins/access&#x27;

const statement = {
  organization: [&#x27;update&#x27;, &#x27;delete&#x27;],
  member: [&#x27;create&#x27;, &#x27;update&#x27;, &#x27;delete&#x27;, &#x27;update-name&#x27;],
  invitation: [&#x27;create&#x27;, &#x27;cancel&#x27;],
} as const

const ac = createAccessControl(statement)

const member = ac.newRole({
  member: [&#x27;update-name&#x27;],
})

const admin = ac.newRole({
  member: [&#x27;update&#x27;, &#x27;delete&#x27;, &#x27;update-name&#x27;],
  invitation: [&#x27;create&#x27;, &#x27;cancel&#x27;],
})

const owner = ac.newRole({
  organization: [&#x27;update&#x27;, &#x27;delete&#x27;],
  member: [&#x27;update&#x27;, &#x27;delete&#x27;, &#x27;update-name&#x27;],
  invitation: [&#x27;create&#x27;, &#x27;cancel&#x27;],
})

export { statement, ac, owner, admin, member }
</code></pre>
<h3>Points importants</h3>
<ul>
<li>Un <strong>utilisateur</strong> peut avoir <a href="https://www.better-auth.com/docs/plugins/organization#roles"><strong>plusieurs rôles à la fois</strong></a>.</li>
<li>Vous pouvez <a href="https://www.better-auth.com/docs/plugins/organization#custom-permissions"><strong>définir vos propres permissions</strong></a> (ex. <code>update-name</code>).</li>
<li>Si vous définissez un <code>ac</code> et des <code>roles</code> personnalisés, <strong>les permissions par défaut de Better Auth <a href="https://www.better-auth.com/docs/plugins/organization#create-roles">sont écrasées</a></strong>. Better Auth s’attend à retrouver certaines actions pour ses checks internes ; il faut donc réintroduire les <a href="https://www.better-auth.com/docs/plugins/organization#permissions"><strong>actions de base</strong></a> si vous souhaitez utiliser les méthodes natives du plugin :<!-- -->
<ul>
<li><code>organization: [&quot;update&quot;, &quot;delete&quot;]</code></li>
<li><code>member: [&quot;create&quot;, &quot;update&quot;, &quot;delete&quot;]</code></li>
<li><code>invitation: [&quot;create&quot;, &quot;cancel&quot;]</code></li>
</ul>
</li>
</ul>
<p>Si une de ces actions est absente, <strong>les rôles</strong> qui ne la possèdent pas ne pourront pas exécuter les méthodes correspondantes (<code>auth.organization.update</code>, <code>auth.organization.inviteMember</code>, etc.) : elles seront <strong>considérées comme non autorisées</strong>.</p>
<h3>Permission personnalisée</h3>
<p>Vous vous demandez peut-être pourquoi nous avons à la fois <code>update</code> et <code>update-name</code> dans nos permissions. Intuitivement, on pourrait penser que <code>update</code> couvre toutes les actions de mise à jour mais dans le plugin Organization, <code>update</code> fait uniquement référence à la méthode <a href="https://www.better-auth.com/docs/plugins/organization#update-member-role"><strong>updateMemberRole</strong></a>, c&#x27;est-à-dire au changement de rôle d&#x27;un membre.</p>
<p>Cela s&#x27;explique par le fait que la table <code>Member</code> ne contient <a href="https://www.better-auth.com/docs/plugins/organization#member">aucun champ que l&#x27;on souhaiterait modifier par défaut</a> autre que <code>role</code>. Notre permission personnalisée <code>update-name</code> n&#x27;aura donc de sens que si nous ajoutons un champ <code>name</code> à la table <code>Member</code> et que nous implémentons la logique métier correspondante (via nos propres routes ou actions serveur).</p>
<h2>Vérifier les permissions côté client et côté serveur</h2>
<p>Une bonne pratique consiste à effectuer <strong>un double check</strong> :</p>
<ul>
<li><strong>Côté client</strong> → pour la logique d’interface (afficher/masquer un bouton)</li>
<li><strong>Côté serveur</strong> → pour la sécurité et la source de vérité</li>
</ul>
<h3>Côté client</h3>
<pre><code class="language-ts">authClient.organization.checkRolePermission({
  permissions: { organization: [&#x27;update&#x27;] },
  role: &#x27;owner&#x27;,
})
</code></pre>
<p>Cette méthode est <strong>synchrone</strong> et ne dépend pas du serveur.
Elle est idéale pour ajuster l’UI selon les permissions du rôle mais ne prend pas en compte les éventuelles mises à jour récentes (changement de rôle, révocation, etc.).</p>
<h3>Côté serveur</h3>
<p>Le serveur reste <strong>la source de vérité</strong>.<br/>
<!-- -->Les rôles et permissions étant stockés en base, il faut toujours valider côté serveur :</p>
<pre><code class="language-ts">await auth.api.hasPermission({
  headers: await headers(),
  body: {
    permissions: { organization: [&#x27;update&#x27;] },
  },
})
</code></pre>
<p>Voici un petit utilitaire ainsi que notre implémentation côtés client/serveur :</p>
<div><div><div value="tab-1">types/permissions.ts</div><div value="tab-2">utils/permissions.ts</div><div value="tab-3">actions/permission.ts</div></div><div value="tab-1"><pre><code class="language-ts">import { statement } from &#x27;@/lib/auth/permissions&#x27;

export type Entities = keyof typeof statement
export type PermissionFor&lt;E extends Entities&gt; = (typeof statement)[E][number]
</code></pre></div><div value="tab-2"><pre><code class="language-ts">// Côté Client
export const hasClientPermission = &lt;E extends Entities, P extends PermissionFor&lt;E&gt;&gt;(
  role: Role,
  entity: E,
  permission: P
) =&gt; {
  return authClient.organization.checkRolePermission({
    permissions: { [entity]: [permission] },
    role: role!,
  })
}
</code></pre></div><div value="tab-3"><pre><code class="language-ts">// Côté Serveur
&#x27;use server&#x27;

import { auth } from &#x27;@/lib/auth&#x27;
import { ERROR_MESSAGES } from &#x27;@/utils/constants&#x27;
import { Entities, PermissionFor } from &#x27;@/utils/organization/permissions&#x27;
import { headers } from &#x27;next/headers&#x27;

export const hasServerPermission = async &lt;E extends Entities, P extends PermissionFor&lt;E&gt;&gt;(
  entity: E,
  permission: P
) =&gt; {
  try {
    const { error, success } = await auth.api.hasPermission({
      headers: await headers(),
      body: {
        permissions: { [entity]: [permission] },
      },
    })

    if (error) {
      throw new Error(error)
    }

    if (success === false) {
      throw new Error(ERROR_MESSAGES.YOU_DONT_HAVE_PERMISSION_TO_DO_THIS_ACTION)
    }

    return success
  } catch (error) {
    if (error instanceof Error) {
      throw new Error(error.message)
    }

    console.error(&#x27;[🔴 hasPermission]: &#x27;, error)
    throw new Error(ERROR_MESSAGES.AN_ERROR_OCCURRED)
  }
}
</code></pre></div></div>
<h2>Gérer les invitations</h2>
<p>Better Auth fournit un système intégré de gestion des <a href="https://www.better-auth.com/docs/plugins/organization#invitations">invitations</a> qui permet d’ajouter de nouveaux membres à une organisation.</p>
<p><img src="https://www.premieroctet.com/blog/better-auth-structure-et-permissions-avec-le-plugin-organization/invitations.jpg" alt="Invitations : capture de l’interface"/></p>
<h3>Les méthodes SDK</h3>
<p>Côté client (<code>authClient</code>) comme côté serveur (<code>auth</code>), le fonctionnement est identique :</p>
<div><div><div value="tab-1">clients/auth.ts</div><div value="tab-2">auth.ts</div></div><div value="tab-1"><pre><code class="language-ts">// Côté client
// ⚠️ Pensez à rafraîchir l’état côté client si nécessaire

const { data, error } = await authClient.organization.listInvitations({
  query: {
  organizationId: &quot;organization-id&quot;,
  },
});

// Côté serveur, la méthode équivalente à inviteMember s’appelle `createInvitation`
const { data, error } = await authClient.organization.inviteMember({
  email: &quot;example@gmail.com&quot;,
  role: &quot;member&quot;,
  organizationId: &quot;org-id&quot;,
  resend: true,
  teamId: &quot;team-id&quot;,
});

const { data, error } = await authClient.organization.acceptInvitation({
  invitationId: &quot;invitation-id&quot;,
});

await authClient.organization.rejectInvitation({
  invitationId: &quot;invitation-id&quot;,
});

await authClient.organization.cancelInvitation({
  invitationId: &quot;invitation-id&quot;,
});

</code></pre></div><div value="tab-2"><pre><code class="language-ts">// Côté serveur
// ⚠️ N&#x27;oubliez pas d&#x27;inclure les headers
// Sinon la méthode renverra un statut 401 &quot;UNAUTHORIZED&quot;

const data = await auth.api.listInvitations({
  headers: await headers(),
  query: {
      organizationId: &quot;organization-id&quot;,
  },
});

await auth.api.createInvitation({
  headers: await headers(),
  body: {
      email: &quot;example@gmail.com&quot;,
      role: &quot;member&quot;,
      organizationId: &quot;org-id&quot;,
      resend: true,
      teamId: &quot;team-id&quot;,
  },
});

await auth.api.acceptInvitation({
  headers: await headers(),
  body: {
  invitationId: &quot;invitation-id&quot;,
  },
});

await auth.api.rejectInvitation({
  headers: await headers(),
  body: {
  invitationId: &quot;invitation-id&quot;,
  },
});

await auth.api.cancelInvitation({
  headers: await headers(),
  body: {
  invitationId: &quot;invitation-id&quot;,
  },
});
</code></pre></div></div>
<h3>Callbacks</h3>
<p>Après la création d&#x27;une invitation avec <code>inviteMember</code> (<strong>authClient</strong>) ou <code>createInvitation</code> (<strong>auth</strong>), Better Auth exécute automatiquement le callback <a href="https://www.better-auth.com/docs/plugins/organization#setup-invitation-email"><strong>sendInvitationEmail</strong></a> qui permet d’envoyer un e-mail personnalisé contenant l’<code>invitationId</code>.</p>
<p>Il existe également un callback <a href="https://www.better-auth.com/docs/plugins/organization#invitation-accepted-callback"><strong>onInvitationAccepted</strong></a> déclenché lorsque l&#x27;utilisateur accepte une invitation :</p>
<pre><code class="language-ts">import { betterAuth } from &#x27;better-auth&#x27;
import { organization } from &#x27;better-auth/plugins&#x27;

export const auth = betterAuth({
  plugins: [
    organization({
      async sendInvitationEmail(data) {
        // Gestion de votre envoi d&#x27;invitation par email
      },
      async onInvitationAccepted(data) {
        // Gestion après qu&#x27;un utilisateur accepte une invitation
      },
    }),
  ],
})
</code></pre>
<p>⚙️ <strong>De notre côté</strong>, dans le mail d&#x27;invitation, nous avons inclus un lien vers une page de redirection <code>/accept-invitation/[invitationId]</code>. Elle redirige l&#x27;utilisateur vers la page de connexion s’il n’est pas authentifié ou vers une page de gestion dédiée où il peut <a href="https://www.better-auth.com/docs/plugins/organization#accept-invitation">accepter</a> ou <a href="https://www.better-auth.com/docs/plugins/organization#cancel-invitation">refuser</a> son invitation.</p>
<h2>Pour aller plus loin : le mode Teams</h2>
<p>Better Auth permet d’ajouter <strong>un niveau hiérarchique supplémentaire</strong> grâce au flag de configuration <a href="https://www.better-auth.com/docs/plugins/organization#teams"><strong>teams</strong></a>. Chaque organisation pourra ainsi avoir différentes équipes, chacune avec ses propres membres et rôles.</p>
<p>C’est particulièrement utile pour les applications complexes où :</p>
<ul>
<li>une entreprise regroupe plusieurs départements,</li>
<li>un utilisateur a des rôles différents selon l’équipe,</li>
<li>on souhaite affiner les permissions sans multiplier les organisations.</li>
</ul>
<pre><code class="language-ts">import { betterAuth } from &#x27;better-auth&#x27;
import { organization } from &#x27;better-auth/plugins&#x27;

export const auth = betterAuth({
  plugins: [
    organization({
      teams: { enabled: true },
    }),
  ],
})
</code></pre>
<p>Better Auth gère automatiquement les relations entre <code>organization</code>, <code>team</code> et <code>member</code> : les utilisateurs pourront faire partie de plusieurs équipes au sein d&#x27;une même organisation. <a href="https://www.better-auth.com/docs/plugins/organization#teams">Le système d’Access Control</a> s’applique également <strong>au niveau de l’équipe</strong>, permettant par exemple de limiter une action à un rôle dans une seule équipe.</p>
<h2>Conclusion</h2>
<p>Le plugin <a href="https://www.better-auth.com/docs/plugins/organization"><strong>Organization</strong></a> de Better Auth offre une flexibilité impressionnante, il permet de construire un système d’accès robuste et lisible. Le système de permissions est <strong>entièrement extensible</strong>, vous pouvez créer vos propres entités (<code>project</code>, <code>team</code>, <code>workspace</code>) et définir autant de types d’actions que nécessaire pour coller à vos besoins métier !</p>
<p>Avec la configuration <strong>Teams</strong>, on peut modéliser des structures hiérarchiques avancées sans perdre la simplicité de son API.</p>
<p>Better Auth ne se limite donc pas à l’authentification, <strong>c’est un véritable socle d’autorisation moderne</strong>, extensible et agréable à utiliser.</p>]]></content:encoded>
            <enclosure url="https://www.premieroctet.com/blog/better-auth-structure-et-permissions-avec-le-plugin-organization/illu.jpg" length="0" type="image/jpg"/>
        </item>
        <item>
            <title><![CDATA[OpenAI Apps SDK : développer des apps natives dans ChatGPT]]></title>
            <link>https://www.premieroctet.com/blog/openai-apps-sdk-developper-des-apps-natives-dans-chatgpt</link>
            <guid>https://www.premieroctet.com/blog/openai-apps-sdk-developper-des-apps-natives-dans-chatgpt</guid>
            <pubDate>Tue, 07 Oct 2025 00:00:00 GMT</pubDate>
            <description><![CDATA[Découvrez l'Apps SDK d'OpenAI : développez des applications natives dans ChatGPT avec TypeScript, React et le Model Context Protocol.]]></description>
            <content:encoded><![CDATA[<p>Imaginez proposer à vos utilisateurs de commander un repas, réserver un trajet ou interagir avec vos APIs directement dans ChatGPT, sans jamais quitter la conversation. C&#x27;est exactement ce que permet l&#x27;<a href="https://developers.openai.com/apps-sdk">Apps SDK</a> d&#x27;OpenAI, fraîchement annoncé.</p>
<p>ChatGPT ne se contente plus d&#x27;être un assistant textuel : il devient une <strong>plateforme d&#x27;applications</strong>, comparable à l&#x27;App Store pour iOS ou au Play Store pour Android.</p>
<h2>Les Apps ChatGPT : pourquoi ?</h2>
<p>L&#x27;Apps SDK d&#x27;OpenAI transforme ChatGPT d&#x27;un simple assistant conversationnel en une <strong>plateforme d&#x27;applications</strong>. Avec plus de 100 millions d&#x27;utilisateurs actifs hebdomadaires, ChatGPT représente désormais un canal de distribution sans précédent pour les entreprises.</p>
<p>Cette transformation ouvre des perspectives commerciales. Les développeurs peuvent désormais créer des applications natives qui s&#x27;intègrent parfaitement dans l&#x27;expérience utilisateur de ChatGPT, offrant un accès direct à une audience engagée.</p>
<p>Comme toute technologie émergente, l&#x27;Apps SDK a ses contraintes. OpenAI est très clair sur les cas d&#x27;usage à éviter :</p>
<ul>
<li>Des contenus longs et complexes (mieux vaut un site web classique)</li>
<li>Des workflows multi-étapes complexes (ChatGPT n&#x27;est pas optimisé pour ça)</li>
<li>De la publicité ou du contenu promotionnel non sollicité (ça dégrade l&#x27;expérience utilisateur)</li>
<li>L&#x27;affichage d&#x27;informations sensibles directement dans la conversation</li>
</ul>
<h2>L&#x27;architecture technique : MCP au cœur</h2>
<h3>Le protocole au cœur de l&#x27;architecture : MCP</h3>
<p>Si vous êtes développeur, voici la bonne nouvelle : l&#x27;Apps SDK ne réinvente pas la roue. Elle s&#x27;appuie sur le <a href="https://modelcontextprotocol.io/">Model Context Protocol</a>, un <strong>standard ouvert</strong> créé par Anthropic et adopté par OpenAI.</p>
<p>Qu&#x27;est-ce que cela signifie concrètement ? Que le code que vous écrivez aujourd&#x27;hui pour ChatGPT pourrait fonctionner demain avec d&#x27;autres clients MCP. C&#x27;est rare dans l&#x27;écosystème IA, et c&#x27;est une excellente nouvelle pour pérenniser vos développements.</p>
<p>MCP définit trois primitives essentielles :</p>
<ol>
<li><strong>List tools</strong> : votre serveur expose les outils disponibles avec leurs schémas</li>
<li><strong>Call tools</strong> : ChatGPT invoque vos outils avec les arguments appropriés</li>
<li><strong>Return components</strong> : chaque outil peut retourner une interface HTML à rendre</li>
</ol>
<p>L&#x27;avantage ? Votre connecteur fonctionnera automatiquement sur ChatGPT web, mobile, et potentiellement sur tous les futurs clients compatibles MCP. Vous construisez une fois, vous déployez partout.</p>
<h3>Choisir sa stack : TypeScript ou Python ?</h3>
<p>Pour développer votre app, vous pouvez utiliser les SDKs MCP officiels :</p>
<p><strong>TypeScript</strong> (notre préféré bien sûr ❤️) :</p>
<ul>
<li><a href="https://github.com/modelcontextprotocol/typescript-sdk">@modelcontextprotocol/sdk</a></li>
<li>Parfait si vous avez déjà une équipe Node.js/React</li>
<li>S&#x27;intègre naturellement avec vos composants web existants</li>
<li>Excellent outillage avec TypeScript, Zod, et l&#x27;écosystème moderne</li>
</ul>
<p><strong>Python</strong> :</p>
<ul>
<li><a href="https://github.com/modelcontextprotocol/python-sdk">modelcontextprotocol/python-sdk</a></li>
<li>Inclut FastMCP pour démarrer rapidement</li>
<li>Idéal pour les équipes data science ou machine learning</li>
<li>Pratique si votre backend est déjà en Python</li>
</ul>
<p>Pour la suite de cet article, nous allons nous concentrer sur <strong>TypeScript</strong>, car c&#x27;est la stack que nous privilégions chez Premier Octet.</p>
<h2>Implémentation technique : passons à la pratique</h2>
<p>Construisons ensemble une app ChatGPT concrète. Nous allons créer un widget d&#x27;articles de blog Premier Octet, du serveur au composant React.</p>
<h3>Étape 1 : Monter le serveur MCP</h3>
<p>La première étape consiste à créer un serveur qui expose nos outils. Voici le code de démarrage :</p>
<pre><code class="language-typescript">import { McpServer } from &#x27;@modelcontextprotocol/sdk/server/mcp.js&#x27;
import { z } from &#x27;zod&#x27;
import { readFileSync } from &#x27;node:fs&#x27;

// Initialiser le serveur MCP
const server = new McpServer({
  name: &#x27;premier-octet-blog&#x27;,
  version: &#x27;1.0.0&#x27;,
})

// Charger les assets compilés du composant React
const BLOG_JS = readFileSync(&#x27;web/dist/blog-widget.js&#x27;, &#x27;utf8&#x27;)
const BLOG_CSS = readFileSync(&#x27;web/dist/blog-widget.css&#x27;, &#x27;utf8&#x27;)

// Enregistrer la ressource UI (template HTML)
server.registerResource(&#x27;blog-widget&#x27;, &#x27;ui://widget/blog-articles.html&#x27;, {}, async () =&gt; ({
  contents: [
    {
      uri: &#x27;ui://widget/blog-articles.html&#x27;,
      mimeType: &#x27;text/html+skybridge&#x27;,
      text: `
        &lt;div id=&quot;blog-root&quot;&gt;&lt;/div&gt;
        ${BLOG_CSS ? `&lt;style&gt;${BLOG_CSS}&lt;/style&gt;` : &#x27;&#x27;}
        &lt;script type=&quot;module&quot;&gt;${BLOG_JS}&lt;/script&gt;
      `.trim(),
    },
  ],
}))
</code></pre>
<p>Rien de compliqué ici : on initialise le serveur, on charge notre bundle React compilé, et on l&#x27;enregistre comme ressource. Le <code>mimeType: &#x27;text/html+skybridge&#x27;</code> est crucial : c&#x27;est le signal qui dit à ChatGPT &quot;voici une interface à afficher&quot;. Skybridge est le runtime sandboxé de ChatGPT.</p>
<h3>Étape 2 : Définir un outil</h3>
<p>Maintenant, créons un outil que ChatGPT pourra invoquer. Vous définissez ce que votre app peut faire, et ChatGPT décide quand l&#x27;utiliser en fonction du contexte de la conversation.</p>
<pre><code class="language-typescript">server.registerTool(
  &#x27;show_blog_articles&#x27;,
  {
    title: &#x27;Afficher les articles du blog Premier Octet&#x27;,
    description:
      &quot;Utilisez cet outil quand l&#x27;utilisateur veut découvrir les derniers articles du blog Premier Octet ou rechercher des articles par thème&quot;,
    inputSchema: {
      type: &#x27;object&#x27;,
      properties: {
        category: {
          type: &#x27;string&#x27;,
          description: &quot;Catégorie d&#x27;articles (ex: react, typescript, openai)&quot;,
        },
        limit: { type: &#x27;number&#x27;, description: &quot;Nombre d&#x27;articles à afficher (défaut: 6)&quot; },
      },
      required: [],
    },
    _meta: {
      &#x27;openai/outputTemplate&#x27;: &#x27;ui://widget/blog-articles.html&#x27;,
      &#x27;openai/widgetAccessible&#x27;: true, // Permet les appels depuis le composant
      &#x27;openai/toolInvocation/invoking&#x27;: &#x27;Chargement des articles...&#x27;,
      &#x27;openai/toolInvocation/invoked&#x27;: &#x27;Articles affichés&#x27;,
    },
  },
  async ({ category, limit = 6 }, context) =&gt; {
    // Récupérer les articles depuis votre API
    const articles = await fetchBlogArticles({ category, limit })

    return {
      // Données structurées pour le modèle ET le composant
      structuredContent: {
        articles: articles.map((article) =&gt; ({
          id: article.id,
          title: article.title,
          excerpt: article.excerpt,
          date: article.date,
          author: article.author,
          tags: article.tags,
          slug: article.slug,
        })),
        total: articles.length,
        category: category || &#x27;tous&#x27;,
      },
      // Texte pour le transcript de conversation
      content: [
        {
          type: &#x27;text&#x27;,
          text: `Voici ${articles.length} articles du blog Premier Octet${
            category ? ` sur le thème &quot;${category}&quot;` : &#x27;&#x27;
          }.`,
        },
      ],
      // Métadonnées privées (invisibles au modèle)
      _meta: {
        fullArticles: articles, // Données complètes pour le UI
        searchQuery: category,
      },
    }
  }
)
</code></pre>
<p>Remarquez comment nous séparons clairement les données :</p>
<ul>
<li><code>structuredContent</code> : ce que le modèle ET le composant voient (gardez ça concis !)</li>
<li><code>content</code> : le texte qui apparaîtra dans la conversation</li>
<li><code>_meta</code> : les données privées, invisibles au modèle mais disponibles pour votre UI</li>
</ul>
<h3>Étape 3 : Construire le composant React</h3>
<p>Vous développez déjà des composants React ? Bonne nouvelle : vous pouvez les utiliser ! La seule différence, c&#x27;est qu&#x27;ils tournent dans une iframe sandboxée et communiquent avec ChatGPT via l&#x27;API <code>window.openai</code>.</p>
<pre><code class="language-tsx">import React, { useEffect, useState } from &#x27;react&#x27;
import { createRoot } from &#x27;react-dom/client&#x27;

interface BlogData {
  articles: Array&lt;{
    id: string
    title: string
    excerpt: string
    date: string
    author: string
    tags: string[]
    slug: string
  }&gt;
  total: number
  category: string
}

function BlogWidget() {
  // Récupérer les données initiales du tool
  const initialData = window.openai?.toolOutput as BlogData
  const [articles, setArticles] = useState(initialData)

  // Persister l&#x27;état local dans ChatGPT
  const saveState = async (newArticles: BlogData) =&gt; {
    setArticles(newArticles)
    await window.openai?.setWidgetState?.({
      version: 1,
      articles: newArticles,
      lastModified: Date.now(),
    })
  }

  // Appeler un outil depuis le composant
  const searchArticles = async (category: string) =&gt; {
    await window.openai?.callTool(&#x27;show_blog_articles&#x27;, {
      category,
      limit: 6,
    })
  }

  // Gérer les changements de layout
  const displayMode = window.openai?.displayMode || &#x27;inline&#x27;
  const maxHeight = window.openai?.maxHeight

  return (
    &lt;div
      style={{ maxHeight }}
      className={displayMode === &#x27;fullscreen&#x27; ? &#x27;fullscreen-layout&#x27; : &#x27;inline-layout&#x27;}
    &gt;
      &lt;div className=&quot;blog-header&quot;&gt;
        &lt;h2&gt;Articles Premier Octet&lt;/h2&gt;
        &lt;div className=&quot;category-filters&quot;&gt;
          &lt;button onClick={() =&gt; searchArticles(&#x27;&#x27;)}&gt;Tous&lt;/button&gt;
          &lt;button onClick={() =&gt; searchArticles(&#x27;react&#x27;)}&gt;React&lt;/button&gt;
          &lt;button onClick={() =&gt; searchArticles(&#x27;typescript&#x27;)}&gt;TypeScript&lt;/button&gt;
          &lt;button onClick={() =&gt; searchArticles(&#x27;openai&#x27;)}&gt;OpenAI&lt;/button&gt;
        &lt;/div&gt;
      &lt;/div&gt;

      &lt;div className=&quot;articles-grid&quot;&gt;
        {articles.articles.map((article) =&gt; (
          &lt;article key={article.id} className=&quot;article-card&quot;&gt;
            &lt;h3&gt;{article.title}&lt;/h3&gt;
            &lt;p className=&quot;article-excerpt&quot;&gt;{article.excerpt}&lt;/p&gt;
            &lt;div className=&quot;article-meta&quot;&gt;
              &lt;span className=&quot;author&quot;&gt;{article.author}&lt;/span&gt;
              &lt;span className=&quot;date&quot;&gt;{new Date(article.date).toLocaleDateString(&#x27;fr-FR&#x27;)}&lt;/span&gt;
            &lt;/div&gt;
            &lt;div className=&quot;article-tags&quot;&gt;
              {article.tags.map((tag) =&gt; (
                &lt;span key={tag} className=&quot;tag&quot;&gt;
                  {tag}
                &lt;/span&gt;
              ))}
            &lt;/div&gt;
          &lt;/article&gt;
        ))}
      &lt;/div&gt;
    &lt;/div&gt;
  )
}

// Monter le composant
createRoot(document.getElementById(&#x27;blog-root&#x27;)!).render(&lt;BlogWidget /&gt;)
</code></pre>
<p>Ce composant illustre les concepts clés :</p>
<ul>
<li>Lecture des données initiales depuis <code>toolOutput</code></li>
<li>Persistance de l&#x27;état avec <code>setWidgetState</code> (pour que l&#x27;état survive aux re-rendus)</li>
<li>Appels d&#x27;outils depuis le composant avec <code>callTool</code> pour filtrer par catégorie</li>
<li>Adaptation au layout (inline vs fullscreen)</li>
<li>Interface utilisateur avec filtres et grille d&#x27;articles</li>
</ul>
<h3>Étape 4 : l&#x27;API window.openai</h3>
<p>L&#x27;API <code>window.openai</code> est votre pont entre le composant et ChatGPT :</p>
<pre><code class="language-typescript">// Données
window.openai.toolInput // Arguments passés à l&#x27;outil
window.openai.toolOutput // Réponse de l&#x27;outil (structuredContent)
window.openai.widgetState // État persisté entre les rendus

// Actions
await window.openai.setWidgetState({
  /* state */
})
await window.openai.callTool(&#x27;tool_name&#x27;, {
  /* args */
})
await window.openai.sendFollowupTurn({ prompt: &#x27;...&#x27; })
await window.openai.requestDisplayMode({ mode: &#x27;fullscreen&#x27; })

// Layout &amp; contexte
window.openai.displayMode // &quot;inline&quot; | &quot;pip&quot; | &quot;fullscreen&quot;
window.openai.maxHeight // Hauteur max disponible
window.openai.locale // &quot;fr-FR&quot;, &quot;en-US&quot;...
window.openai.theme // &quot;light&quot; | &quot;dark&quot;
</code></pre>
<p>Ces méthodes couvrent 90% de vos besoins. Le reste de la documentation officielle complètera pour les cas avancés.</p>
<h3>Étape 5 : sécuriser avec OAuth 2.1</h3>
<p>Si votre app accède à des données utilisateur (ce qui sera souvent le cas), vous devez implémenter l&#x27;authentification. Bonne nouvelle : l&#x27;Apps SDK supporte OAuth 2.1 de bout en bout, avec vérification automatique des tokens.</p>
<pre><code class="language-typescript">import { FastMCP } from &#x27;@modelcontextprotocol/sdk/server/fastmcp.js&#x27;

// Configurer l&#x27;authentification
const mcp = new FastMCP({
  name: &#x27;secure-blog&#x27;,
  auth: {
    issuerUrl: &#x27;https://your-tenant.auth0.com&#x27;,
    resourceServerUrl: &#x27;https://api.example.com/mcp&#x27;,
    requiredScopes: [&#x27;blog:read&#x27;, &#x27;blog:write&#x27;],
  },
})

// Vérifier les tokens sur chaque appel
mcp.registerTool(
  &#x27;show_blog_articles&#x27;,
  {
    /* ... */
  },
  async ({ projectId }, { token }) =&gt; {
    // Le token est automatiquement vérifié
    const userId = token.subject
    const hasAccess = token.scopes.includes(&#x27;blog:read&#x27;)

    if (!hasAccess) {
      throw new Error(&#x27;Insufficient permissions&#x27;)
    }

    // Charger les données de l&#x27;utilisateur
    return await loadUserBoard(userId, projectId)
  }
)
</code></pre>
<p>Pour accélérer l&#x27;implémentation, utilisez <a href="https://www.premieroctet.com/blog/better-auth-futur-authjs">Better Auth</a>. Il supporte OAuth 2.1, l&#x27;enregistrement dynamique des clients, et la gestion des scopes nativement.</p>
<h2>Rendre votre app découvrable</h2>
<p>La découverte de votre app par les utilisateurs est un enjeu crucial.</p>
<h3>Les différents chemins vers votre app</h3>
<p>ChatGPT offre plusieurs façons aux utilisateurs de découvrir et d&#x27;utiliser votre app :</p>
<ol>
<li><strong>Mention explicite</strong> : &quot;Montre-moi les articles du blog de Premier Octet&quot;</li>
<li><strong>Découverte conversationnelle</strong> : le modèle choisit votre app selon le contexte</li>
<li><strong>Directory</strong> : répertoire des apps avec métadonnées et captures d&#x27;écran</li>
<li><strong>Launcher</strong> : bouton + dans le composer</li>
</ol>
<p>La <strong>qualité de vos métadonnées</strong> fait toute la différence. ChatGPT décide d&#x27;appeler votre outil en analysant sa description. Comparez :</p>
<pre><code class="language-typescript">// ✅ Bon : description action-oriented et contextuelle
description: &quot;Utilisez cet outil quand l&#x27;utilisateur veut visualiser les articles du blog de Premier Octet&quot;

// ❌ Mauvais : trop vague, le modèle ne saura pas quand l&#x27;utiliser
description: &#x27;Un outil pour afficher des articles&#x27;
</code></pre>
<h2>Sécurité et vie privée</h2>
<p>Une app ChatGPT a accès à des données utilisateur sensibles. OpenAI impose des standards stricts de sécurité, et vous devriez en faire autant.</p>
<h3>Les principes de base</h3>
<p>OpenAI impose des standards stricts :</p>
<ol>
<li><strong>Principe du moindre privilège</strong> : ne demandez que les scopes nécessaires</li>
<li><strong>Consentement explicite</strong> : les utilisateurs doivent comprendre ce qu&#x27;ils autorisent</li>
<li><strong>Défense en profondeur</strong> : validez tout côté serveur, même si le modèle l&#x27;a fourni</li>
<li><strong>Minimisation des données</strong> : ne collectez que ce qui est strictement nécessaire</li>
</ol>
<h3>Sandboxing des composants</h3>
<p>Les composants s&#x27;exécutent dans une iframe avec CSP stricte :</p>
<pre><code class="language-typescript">// Définir votre CSP
server.registerResource(&#x27;blog-widget&#x27;, &#x27;ui://widget/blog-articles.html&#x27;, {}, async () =&gt; ({
  contents: [
    {
      uri: &#x27;ui://widget/blog-articles.html&#x27;,
      mimeType: &#x27;text/html&#x27;,
      text: componentHtml,
      _meta: {
        &#x27;openai/widgetCSP&#x27;: {
          connect_domains: [&#x27;https://api.example.com&#x27;],
          resource_domains: [&#x27;https://cdn.example.com&#x27;],
        },
      },
    },
  ],
}))
</code></pre>
<h2>Pourquoi TypeScript est votre meilleur allié</h2>
<p>On en a parlé au début, mais revenons-y : <strong>TypeScript est vraiment le choix optimal</strong> pour l&#x27;Apps SDK. Voici pourquoi :</p>
<h3>Les avantages concrets</h3>
<ol>
<li><strong>Type safety</strong> : Zod + TypeScript vous évitent les bugs. Vos schémas serveur et client sont toujours cohérents.</li>
<li><strong>Réutilisabilité</strong> : vos composants React existants fonctionnent directement.</li>
<li><strong>Tooling performant</strong> : esbuild compile en millisecondes, TypeScript détecte les erreurs avant l&#x27;exécution</li>
<li><strong>Isomorphisme</strong> : un seul langage, des types partagés serveur/client, zéro friction</li>
</ol>
<h3>Exemple : types partagés</h3>
<pre><code class="language-typescript">// shared/types.ts
import { z } from &#x27;zod&#x27;

export const ArticleSchema = z.object({
  id: z.string(),
  title: z.string(),
  excerpt: z.string(),
  date: z.string(),
  author: z.string(),
  tags: z.array(z.string()),
  slug: z.string(),
})

export const BlogDataSchema = z.object({
  articles: z.array(ArticleSchema),
  total: z.number(),
  category: z.string(),
})

export type Article = z.infer&lt;typeof ArticleSchema&gt;
export type BlogData = z.infer&lt;typeof BlogDataSchema&gt;

// Serveur
server.registerTool(
  &#x27;show_blog_articles&#x27;,
  {
    inputSchema: z.object({
      category: z.string().optional(),
      limit: z.number().optional(),
    }),
  },
  async ({ category, limit = 6 }) =&gt; {
    const blogData: BlogData = await fetchBlogArticles({ category, limit })
    return { structuredContent: blogData }
  }
)

// Client React
function BlogWidget() {
  const blogData = window.openai?.toolOutput as BlogData
  // TypeScript sait que blogData.articles existe !
}
</code></pre>
<p>Vous voyez l&#x27;idée : un type défini une fois, utilisé partout. Zéro duplication, zéro désynchronisation.</p>
<h2>L&#x27;UX comme critère de succès</h2>
<p>Une excellente tech ne suffit pas si l&#x27;expérience utilisateur est mauvaise. OpenAI a publié des <a href="https://developers.openai.com/apps-sdk/concepts/design-guidelines">design guidelines</a> très complètes. Voici ce qu&#x27;il faut retenir.</p>
<h3>Les trois modes d&#x27;affichage</h3>
<p>L&#x27;Apps SDK vous permet de choisir comment votre app s&#x27;affiche. Chaque mode a son usage :</p>
<ol>
<li><strong>Inline</strong> : une carte légère dans la conversation — parfait pour une action simple (confirmer une réservation)</li>
<li><strong>Picture-in-Picture</strong> : une fenêtre flottante qui reste visible — idéal pour du contenu continu (vidéo, jeu)</li>
<li><strong>Fullscreen</strong> : une vue immersive avec le composer ChatGPT intégré — pour de l&#x27;édition complexe ou de l&#x27;exploration</li>
</ol>
<pre><code class="language-tsx">function BlogWidget() {
  const displayMode = window.openai?.displayMode

  if (displayMode === &#x27;fullscreen&#x27;) {
    return &lt;FullscreenBlogView /&gt;
  }

  return (
    &lt;InlineCard&gt;
      &lt;button
        onClick={() =&gt; {
          window.openai?.requestDisplayMode({ mode: &#x27;fullscreen&#x27; })
        }}
      &gt;
        Ouvrir en plein écran
      &lt;/button&gt;
    &lt;/InlineCard&gt;
  )
}
</code></pre>
<p>Le passage d&#x27;un mode à l&#x27;autre doit être fluide. Testez bien les trois cas.</p>
<h3>Les principes de design à respecter</h3>
<p>OpenAI insiste sur 5 principes fondamentaux. Ils peuvent sembler évidents, mais en pratique, beaucoup d&#x27;apps les ignorent :</p>
<ul>
<li><strong>Conversationnel</strong> : votre app doit prolonger ChatGPT, pas créer une rupture</li>
<li><strong>Intelligent</strong> : utilisez le contexte de la conversation pour adapter l&#x27;interface</li>
<li><strong>Simple</strong> : une action claire par interaction — ne surchargez pas</li>
<li><strong>Responsive</strong> : ça doit être rapide. Si votre tool met 3 secondes à répondre, c&#x27;est trop</li>
<li><strong>Accessible</strong> : dark mode, tailles de texte, lecteurs d&#x27;écran — faites les choses bien</li>
</ul>
<p>Gardez ces 5 principes constamment à l&#x27;esprit lors du développement.</p>
<h2>Tester et débugger efficacement</h2>
<h3>MCP Inspector : votre meilleur ami</h3>
<p>Avant même de toucher à ChatGPT, vous devez tester localement. Le <strong>MCP Inspector</strong> est l&#x27;outil indispensable :</p>
<pre><code class="language-bash">npx @modelcontextprotocol/inspector@latest
# Pointer vers http://localhost:3000/mcp
</code></pre>
<p>L&#x27;inspector permet de :</p>
<ul>
<li>Lister tous les outils exposés</li>
<li>Appeler les outils avec des paramètres</li>
<li>Visualiser les réponses JSON</li>
<li>Tester le rendu des composants</li>
</ul>
<h3>Tests automatisés</h3>
<pre><code class="language-typescript">import { describe, it, expect } from &#x27;vitest&#x27;
import { server } from &#x27;./server.js&#x27;

describe(&#x27;Blog Articles Tool&#x27;, () =&gt; {
  it(&#x27;should return articles data&#x27;, async () =&gt; {
    const result = await server.callTool(&#x27;show_blog_articles&#x27;, {
      category: &#x27;react&#x27;,
      limit: 3,
    })

    expect(result.structuredContent).toHaveProperty(&#x27;articles&#x27;)
    expect(result.structuredContent.articles).toBeInstanceOf(Array)
    expect(result.structuredContent.total).toBeGreaterThan(0)
  })

  it(&#x27;should filter by category&#x27;, async () =&gt; {
    const result = await server.callTool(&#x27;show_blog_articles&#x27;, {
      category: &#x27;typescript&#x27;,
    })

    expect(result.structuredContent.category).toBe(&#x27;typescript&#x27;)
  })
})
</code></pre>
<h3>Debug dans ChatGPT</h3>
<p>En mode développeur :</p>
<ol>
<li>Ouvrir les DevTools du navigateur</li>
<li>Les composants s&#x27;affichent dans une iframe</li>
<li>Les erreurs apparaissent dans la console</li>
<li>Utiliser <code>console.log</code> dans vos composants</li>
</ol>
<h2>Cas d&#x27;usage réels : inspiration</h2>
<p>OpenAI fournit l&#x27;app de démonstration <strong>Pizzaz</strong> avec plusieurs exemples :</p>
<ul>
<li><strong>Pizzaz List</strong> : liste classée de restaurants</li>
<li><strong>Pizzaz Map</strong> : carte interactive Mapbox</li>
<li><strong>Pizzaz Carousel</strong> : galerie horizontale</li>
<li><strong>Pizzaz Video</strong> : lecteur vidéo avec timeline</li>
<li><strong>Pizzaz Album</strong> : vue détaillée d&#x27;un lieu</li>
</ul>
<p><a href="https://github.com/openai/openai-apps-sdk-examples">Code source disponible</a></p>
<h2>Pour conclure</h2>
<p>L&#x27;<strong>Apps SDK</strong> transforme ChatGPT : d&#x27;un chatbot à une <strong>plateforme d&#x27;applications conversationnelles</strong>.</p>
<p>Techniquement, c&#x27;est bien pensé. L&#x27;architecture MCP, le support TypeScript, et l&#x27;intégration React rendent le développement accessible à toute équipe web moderne. Les guidelines strictes, bien que contraignantes, garantissent une expérience utilisateur cohérente.</p>
<p>Si vous envisagez de construire une app ChatGPT, ou simplement d&#x27;explorer le sujet, <a href="https://www.premieroctet.com/contact">parlons-en ensemble</a>.</p>
<p>👉 Ressources utiles :</p>
<ul>
<li><a href="https://developers.openai.com/apps-sdk">Documentation officielle Apps SDK</a></li>
<li><a href="https://modelcontextprotocol.io/">Model Context Protocol</a></li>
<li><a href="https://github.com/openai/openai-apps-sdk-examples">Exemples GitHub</a></li>
<li><a href="https://github.com/modelcontextprotocol/typescript-sdk">TypeScript SDK</a></li>
</ul>
<p>👋</p>]]></content:encoded>
            <enclosure url="https://www.premieroctet.com/blog/openai-apps-sdk-developper-des-apps-natives-dans-chatgpt/illu.jpg" length="0" type="image/jpg"/>
        </item>
        <item>
            <title><![CDATA[Better Auth : le futur d’Auth.js]]></title>
            <link>https://www.premieroctet.com/blog/better-auth-futur-authjs</link>
            <guid>https://www.premieroctet.com/blog/better-auth-futur-authjs</guid>
            <pubDate>Mon, 06 Oct 2025 00:00:00 GMT</pubDate>
            <description><![CDATA[Retour d’expérience sur Better Auth : installation, plugins et migration depuis Supabase.]]></description>
            <content:encoded><![CDATA[<p>Chez <strong>Premier Octet</strong>, on explore en continu de nouvelles solutions pour simplifier et sécuriser nos développements. Pendant longtemps, <a href="https://next-auth.js.org/">NextAuth.js</a> (devenu <a href="https://authjs.dev/">Auth.js</a>) a été notre référence pour gérer l’authentification dans nos projets <a href="https://nextjs.org/"><strong>Next.js</strong></a>.</p>
<p>Très récemment, un tournant important a eu lieu : <a href="https://www.better-auth.com/blog/authjs-joins-better-auth"><strong>Auth.js rejoint Better Auth</strong></a>.</p>
<p><a href="https://www.better-auth.com/">Better Auth</a> devient donc le successeur officiel d&#x27;<strong>Auth.js</strong> et nous propose une vision plus large ainsi qu&#x27;une approche plus moderne de l’authentification.</p>
<p>Dans cet article, nous partageons notre retour d’expérience : installation, migration depuis Supabase et découverte des plugins.</p>
<h2>Sessions : une évolution d’approche</h2>
<p>Historiquement, <strong>Auth.js</strong> proposait <a href="https://authjs.dev/concepts/session-strategies#jwt-session">deux stratégies</a> :</p>
<ol>
<li>des <strong>sessions stateless</strong> avec un token JWT qui ne nécessitent pas de base de données. Une stratégie légère et pratique, mais si les données de la session évoluent (rôles, permissions…), il faut mettre en place des mécanismes supplémentaires pour invalider les tokens devenus obsolètes. <strong>Cette stratégie est celle appliquée par défaut.</strong></li>
<li>des <strong>sessions stateful</strong> stockées en base de données via un adapter (Prisma, Mongo…) et vérifiées à chaque requête. Elles permettent de garder les permissions constamment à jour, d’invalider immédiatement une session (ex. logout forcé) et de gérer facilement plusieurs appareils. En contrepartie, cette stratégie nécessite plus de ressources, puisqu’elle implique des appels serveur plus fréquents.</li>
</ol>
<p><strong>Better Auth</strong>, lui, a fait le choix d’un modèle basé avant tout sur des <strong>sessions stateful</strong>. Il propose toutefois un plugin <a href="https://www.better-auth.com/docs/plugins/bearer">Bearer Token Authentication</a> qui permet de mettre en place des <strong>sessions stateless</strong>.</p>
<h2>Installation et gestion de la base de données</h2>
<p>Là où Auth.js reposait sur des schémas à intégrer manuellement, Better Auth fournit un <strong>CLI qui génère directement les tables nécessaires</strong>. Résultat : une installation rapide, standardisée et avec moins de risques d’erreurs.</p>
<p>En pratique, voici la mise en place avec Prisma après avoir installé la librairie :</p>
<pre><code class="language-bash">npx @better-auth/cli generate
npx prisma migrate dev
</code></pre>
<div><div><div value="auth.ts">auth.ts</div><div value="clients/auth.ts">clients/auth.ts</div><div value="api/auth/[...all]/route.ts">api/auth/[...all]/route.ts</div></div><div value="auth.ts"><pre><code class="language-tsx">import { betterAuth } from &quot;better-auth&quot;;
import { prismaAdapter } from &quot;better-auth/adapters/prisma&quot;;
import { PrismaClient } from &quot;@prisma/client&quot;;

const prisma = new PrismaClient();

export const auth = betterAuth({
  secret: process.env.BETTER_AUTH_SECRET,
  database: prismaAdapter(prisma, {
    provider: &quot;postgresql&quot;,
  }),
});
</code></pre></div><div value="clients/auth.ts"><pre><code class="language-tsx">import { createAuthClient } from &quot;better-auth/react&quot;;

const authClient = createAuthClient();

export const {
  useSession,
  getSession,
  signIn,
  signOut,
  changeEmail,
  changePassword,
  updateUser,
} = authClient;
</code></pre></div><div value="api/auth/[...all]/route.ts"><pre><code class="language-tsx">import { auth } from &quot;@/auth&quot;;
import { toNextJsHandler } from &quot;better-auth/next-js&quot;;

export const { GET, POST } = toNextJsHandler(auth.handler);
</code></pre></div></div>
<p>Et voilà. ✨ Inscription, connexion, gestion des sessions, modification d’email, réinitialisation de mot de passe, <strong>tout est fonctionnel</strong> et accessible depuis l&#x27;API via les appels <code>auth.api</code> pour le côté serveur et <code>authClient</code> pour le côté client.</p>
<p>Better Auth ne fournit pas d&#x27;UI prête à l&#x27;emploi mais après une <a href="https://www.better-auth.com/docs/authentication/email-password#enable-email-and-password">configuration rapide</a>, ses méthodes permettent une intégration sans prise de tête, que ce soit pour la connexion par <a href="https://www.better-auth.com/docs/authentication/email-password">credentials</a> ou via des providers sociaux comme <a href="https://www.better-auth.com/docs/authentication/google">Google</a>, <a href="https://www.better-auth.com/docs/authentication/apple">Apple</a>, <a href="https://www.better-auth.com/docs/authentication/github">GitHub</a>, etc.</p>
<h2>Migration depuis Supabase</h2>
<p>Nous avons également testé la <a href="https://www.better-auth.com/docs/guides/supabase-migration-guide">migration depuis Supabase</a> sur l&#x27;une de nos applications. Encore une fois, le processus est très simple :</p>
<ol>
<li>Récupérer l’URL de la base de données sur Supabase.</li>
<li>Générer les tables Better Auth avec le CLI.</li>
<li>Lancer le <a href="https://www.better-auth.com/docs/guides/supabase-migration-guide#copy-the-migration-script">script officiel</a> pour convertir les utilisateurs.</li>
</ol>
<p>⚠️ <strong>Limite actuelle</strong> : <a href="https://supabase.com/docs/guides/auth/password-security#how-are-passwords-stored">Supabase utilise <strong>bcrypt</strong></a> pour le hachage des mots de passe tandis que <a href="https://www.better-auth.com/docs/authentication/email-password#configuration">Better Auth repose sur <strong>scrypt</strong></a>. Les utilisateurs devront donc <strong>réinitialiser leur mot de passe</strong> après migration. En production, une communication claire (mailing, bannière, etc.) est indispensable.</p>
<h2>Plugins : aller plus loin</h2>
<p>Tandis qu&#x27;Auth.js se concentrait sur les providers (Google, GitHub, Credentials, etc.), Better Auth propose en plus des <strong>plugins</strong> qui adressent des <strong>besoins métiers concrets</strong> : organisations, rôles avancés, 2FA, magic links.</p>
<p><strong>Exemple</strong> : <a href="https://www.better-auth.com/docs/plugins/organization"><strong>Organization</strong></a> est un plugin qui facilite la gestion d’équipes ou de structures dans une application. Il génère automatiquement les tables nécessaires et expose des méthodes prêtes à l’emploi pour :</p>
<ul>
<li>créer une organisation,</li>
<li>inviter des membres,</li>
<li>attribuer et modifier des rôles,</li>
<li>gérer les droits d’accès associés.</li>
</ul>
<p>En pratique, cela permet de mettre en place un système complet de gestion multi-utilisateurs sans avoir à réécrire nous-mêmes toute la logique métier.</p>
<div><div><div value="auth.ts">auth.ts</div><div value="clients/auth.ts">clients/auth.ts</div></div><div value="auth.ts"><pre><code class="language-tsx">import { betterAuth } from &quot;better-auth&quot;
import { organization } from &quot;better-auth/plugins&quot;

export const auth = betterAuth({
    plugins: [organization()]
})
</code></pre></div><div value="clients/auth.ts"><pre><code class="language-tsx">import { createAuthClient } from &quot;better-auth/client&quot;
import { organizationClient } from &quot;better-auth/client/plugins&quot;

export const authClient = createAuthClient({
    plugins: [organizationClient()]
})
</code></pre></div></div>
<p>En quelques lignes seulement, les plugins ouvrent la porte à des fonctionnalités avancées : <a href="https://www.better-auth.com/docs/plugins/2fa">2FA</a> pour renforcer la sécurité, <a href="https://www.better-auth.com/docs/plugins/magic-link">magic links</a> pour simplifier la connexion, des <a href="https://www.better-auth.com/docs/plugins/admin">outils de gestion administrateur</a> (dont la très utile <a href="https://www.better-auth.com/docs/plugins/admin#impersonate-user">impersonification</a>), et bien plus encore !</p>
<h2>Conclusion</h2>
<p>Tout au long de cet article, le mot <strong>simple</strong> est revenu plus d’une fois et ce n’est pas un hasard. Better Auth a été conçu avec la <strong>Developper Experience</strong> en tête et cela se ressent dès la première installation : tout est fluide et agréable à utiliser.</p>
<p>Au-delà de cette facilité d’usage, Better Auth est désormais le <strong>successeur officiel d’Auth.js</strong>. C’est un outil solide qui combine <strong>sécurité, extensibilité et rapidité de mise en place</strong>.</p>
<p>Chez Premier Octet, Better Auth est désormais notre solution de référence pour gérer l’authentification dans les projets clients. Sa simplicité d’intégration et sa flexibilité en font un choix naturel pour nos développements.</p>
<p>👉 Si vous utilisez déjà Auth.js, <a href="https://www.better-auth.com/"><strong>Better Auth</strong></a> vaut clairement la <a href="https://www.better-auth.com/docs/guides/next-auth-migration-guide">migration</a>.</p>]]></content:encoded>
            <enclosure url="https://www.premieroctet.com/blog/better-auth-futur-authjs/illu.jpg" length="0" type="image/jpg"/>
        </item>
        <item>
            <title><![CDATA[E-commerce conversationnel avec OpenAI et Stripe : guide complet de l’Agentic Commerce Protocol]]></title>
            <link>https://www.premieroctet.com/blog/ecommerce-conversationnel-avec-openai-et-stripe-guide-complet-de-l-agentic-commerce-protocol</link>
            <guid>https://www.premieroctet.com/blog/ecommerce-conversationnel-avec-openai-et-stripe-guide-complet-de-l-agentic-commerce-protocol</guid>
            <pubDate>Wed, 01 Oct 2025 00:00:00 GMT</pubDate>
            <description><![CDATA[Découvrez comment intégrer l’Agentic Commerce Protocol d’OpenAI et Stripe pour vendre directement dans ChatGPT. Guide technique, roadmap et bonnes pratiques.]]></description>
            <content:encoded><![CDATA[<p>OpenAI et Stripe ont annoncé l’<strong>Agentic Commerce Protocol (ACP)</strong>, une spécification open source (Apache 2.0) qui permet d’acheter directement depuis une conversation dans <strong>ChatGPT</strong>, sans passer par un site tiers.</p>
<p>Concrètement, un utilisateur peut demander un produit dans ChatGPT, voir une fiche détaillée et finaliser son achat via un bouton “Acheter”. Côté marchand, cela implique de mettre en place trois briques techniques :</p>
<ul>
<li>un <strong>flux produit</strong> (Product Feed) pour rendre le catalogue accessible,</li>
<li>une <strong>API Checkout</strong> pour gérer les sessions et les commandes,</li>
<li>un mécanisme de <strong>paiement délégué</strong> qui s’appuie notamment sur Stripe.</li>
</ul>
<p>L’accès est pour l’instant limité aux États-Unis et nécessite une validation d’OpenAI, mais l’annonce donne une idée claire de la direction que prend l’e-commerce conversationnel.</p>
<p>Dans cet article, nous expliquons en détail comment fonctionne le protocole, ses trois piliers techniques, et proposons une <strong>roadmap de préparation</strong> pour anticiper une intégration future.</p>
<h2>Qu’est-ce que l’Agentic Commerce Protocol ?</h2>
<p>L’ACP est une spécification ouverte qui définit comment un système tiers (ici ChatGPT) peut <strong>afficher un catalogue produit, initier un checkout et déléguer le paiement</strong> à un PSP (Payment Service Provider) comme <strong>Stripe</strong>.</p>
<p>👉 L’objectif : permettre à vos clients de passer <strong>de la recherche au paiement en un seul flux conversationnel</strong>.</p>
<h3>Comment fonctionne l’e-commerce conversationnel dans ChatGPT ?</h3>
<p>Prenons l&#x27;exemple d&#x27;un utilisateur qui demanderait : &quot;Trouve-moi des chaussures de running à moins de 100 €&quot;.</p>
<p>ChatGPT peut alors :</p>
<ol>
<li>Afficher les produits de votre catalogue (via le Product Feed)</li>
<li>Proposer un bouton “Acheter”</li>
<li>Déclencher un <strong>checkout complet directement dans la conversation</strong></li>
</ol>
<p>Trois flux de données orchestrent ce parcours :</p>
<ul>
<li><strong>Product Feed</strong> : ChatGPT indexe vos produits pour les rendre découvrables</li>
<li><strong>API Checkout</strong> : vous gérez les sessions panier et la finalisation des commandes</li>
<li><strong>Paiement délégué</strong> : vos PSP gèrent la transaction en toute sécurité</li>
</ul>
<h2>Pilier 1 : Le Product Feed 📦</h2>
<p>Premier élément à mettre en place : le <strong>Product Feed</strong>. Avant que ChatGPT puisse recommander vos produits, il faut lui fournir un catalogue structuré et à jour. Sans ça, même si votre API Checkout est parfaite, vos produits resteront invisibles.</p>
<h3>Le format</h3>
<p>Vous avez le choix : <strong>TSV, CSV, XML ou JSON</strong>. Prenez celui qui s&#x27;intègre le mieux à votre système existant. Nous recommandons JSON pour sa flexibilité et sa lisibilité, mais CSV fera très bien l&#x27;affaire si c&#x27;est ce que votre système génère naturellement.</p>
<p>Le feed est poussé vers un endpoint OpenAI qui vous sera fourni lors de votre onboarding. Pas de pull côté OpenAI, c&#x27;est vous qui envoyez les mises à jour.</p>
<div type="info"><p>Fréquence de mise à jour : toutes les 15 minutes si nécessaire. C&#x27;est important pour maintenir la
cohérence entre votre stock réel et ce que ChatGPT affiche. Un produit en rupture ne doit pas être
proposé à l&#x27;achat.</p></div>
<h3>Les champs</h3>
<p>La structure est assez classique. Voici un exemple de produit que OpenAI attend :</p>
<pre><code class="language-json">[
  {
    &quot;id&quot;: &quot;SKU12345&quot;,
    &quot;title&quot;: &quot;Chaussures de running Nike Air Zoom Pegasus&quot;,
    &quot;description&quot;: &quot;Chaussures légères avec amorti réactif...&quot;,
    &quot;link&quot;: &quot;https://yourshop.com/products/SKU12345&quot;,
    &quot;price&quot;: &quot;89.99 EUR&quot;,
    &quot;currency&quot;: &quot;eur&quot;,
    &quot;availability&quot;: &quot;in_stock&quot;,
    &quot;image_link&quot;: &quot;https://yourshop.com/images/SKU12345.jpg&quot;,
    &quot;brand&quot;: &quot;Nike&quot;,
    &quot;condition&quot;: &quot;new&quot;,
    &quot;weight&quot;: &quot;280 g&quot;,
    &quot;enable_search&quot;: true,
    &quot;enable_checkout&quot;: true
  }
]
</code></pre>
<p>Les deux derniers flags (<code>enable_search</code> et <code>enable_checkout</code>) sont spécifiques à OpenAI et vous donnent un contrôle granulaire : un produit peut être découvrable dans la recherche sans être achetable directement.</p>
<p>Vous pouvez retrouver la liste complète des champs dans la <a href="https://developers.openai.com/commerce/specs/feed">documentation d&#x27;OpenAI</a>.</p>
<h3>Gérer les variantes</h3>
<p>Point important si vous vendez des produits déclinables : les variantes. Si vous vendez une chaussure en plusieurs tailles et couleurs, utilisez <code>item_group_id</code> pour regrouper les variantes. Concrètement, ça ressemble à ça :</p>
<pre><code class="language-json">[
  {
    &quot;id&quot;: &quot;SKU12345-42-BLUE&quot;,
    &quot;item_group_id&quot;: &quot;SKU12345&quot;,
    &quot;title&quot;: &quot;Nike Air Zoom Pegasus - Bleu&quot;,
    &quot;color&quot;: &quot;Bleu&quot;,
    &quot;size&quot;: &quot;42&quot;
    // ...
  },
  {
    &quot;id&quot;: &quot;SKU12345-43-BLUE&quot;,
    &quot;item_group_id&quot;: &quot;SKU12345&quot;,
    &quot;title&quot;: &quot;Nike Air Zoom Pegasus - Bleu&quot;,
    &quot;color&quot;: &quot;Bleu&quot;,
    &quot;size&quot;: &quot;43&quot;
    // ...
  }
]
</code></pre>
<p>ChatGPT saura alors proposer les différentes options au moment de l&#x27;achat.</p>
<h2>Pilier 2 : L&#x27;API Checkout 🛍️</h2>
<p>Une fois vos produits indexés dans ChatGPT, il faut gérer la partie achat. C&#x27;est le cœur du système, et c&#x27;est là que vous allez passer le plus de temps de développement.</p>
<p>Vous devez exposer 5 endpoints que ChatGPT va appeler pour gérer le cycle de vie d&#x27;un achat. L&#x27;idée est simple : ChatGPT orchestre, mais c&#x27;est vous qui calculez tout (prix, taxes, shipping) et qui décidez in fine d&#x27;accepter ou non la commande.</p>
<h3>1. Créer une session</h3>
<pre><code>POST /checkout_sessions
</code></pre>
<p>C&#x27;est le point d&#x27;entrée. Quand l&#x27;utilisateur clique sur &quot;Acheter&quot; dans ChatGPT, cet endpoint est appelé avec les items que l&#x27;utilisateur veut acheter et éventuellement son adresse de livraison :</p>
<p><strong>Requête :</strong></p>
<pre><code class="language-json">{
  &quot;items&quot;: [{ &quot;id&quot;: &quot;SKU12345-42-BLUE&quot;, &quot;quantity&quot;: 1 }],
  &quot;fulfillment_address&quot;: {
    &quot;name&quot;: &quot;Bptiste Adrien&quot;,
    &quot;line_one&quot;: &quot;18 avenue Parementier&quot;,
    &quot;city&quot;: &quot;Paris&quot;,
    &quot;postal_code&quot;: &quot;75018&quot;,
    &quot;country&quot;: &quot;FR&quot;
  }
}
</code></pre>
<h3>2. Mettre à jour la session</h3>
<pre><code>POST /checkout_sessions/{checkout_session_id}
</code></pre>
<p>L&#x27;utilisateur change d&#x27;avis sur son mode de livraison ou modifie son adresse ? Pas de problème. ChatGPT appelle cet endpoint avec les nouvelles données, vous recalculez taxes et frais de port, et vous retournez le même format de réponse que lors de la création.</p>
<p>C&#x27;est important que ce endpoint soit rapide, car l&#x27;utilisateur peut le déclencher plusieurs fois en ajustant ses choix.</p>
<h3>3. Finaliser l&#x27;achat</h3>
<pre><code>POST /checkout_sessions/{checkout_session_id}/complete
</code></pre>
<p>C&#x27;est le moment critique : l&#x27;utilisateur a validé son paiement.</p>
<p><strong>Requête :</strong></p>
<pre><code class="language-json">{
  &quot;buyer&quot;: {
    &quot;first_name&quot;: &quot;Baptiste&quot;,
    &quot;last_name&quot;: &quot;Adrien&quot;,
    &quot;email&quot;: &quot;hello@premieroctet.com&quot;
  },
  &quot;payment_data&quot;: {
    &quot;token&quot;: &quot;spt_1234567890&quot;,
    &quot;provider&quot;: &quot;stripe&quot;,
    &quot;billing_address&quot;: {
      /* ... */
    }
  }
}
</code></pre>
<p>À ce moment, vous :</p>
<ol>
<li>Chargez le moyen de paiement via votre PSP (Stripe, etc.)</li>
<li>Créez la commande dans votre système</li>
<li>Retournez l&#x27;ID de commande et un lien de suivi</li>
</ol>
<p><strong>Réponse :</strong></p>
<pre><code class="language-json">{
  &quot;id&quot;: &quot;checkout_session_abc123&quot;,
  &quot;status&quot;: &quot;completed&quot;,
  &quot;order&quot;: {
    &quot;id&quot;: &quot;order_789&quot;,
    &quot;permalink_url&quot;: &quot;https://yourshop.com/orders/order_789&quot;
  }
  // ... reste de l&#x27;état du checkout
}
</code></pre>
<p>Le <code>permalink_url</code> est crucial : c&#x27;est là que l&#x27;utilisateur pourra suivre sa commande.</p>
<h3>4 &amp; 5. Récupérer et annuler</h3>
<pre><code>GET /checkout_sessions/{checkout_session_id}
POST /checkout_sessions/{checkout_session_id}/cancel
</code></pre>
<p>Ces deux derniers endpoints sont plus simples. Le GET permet à ChatGPT de vérifier l&#x27;état actuel d&#x27;une session (utile en cas de reconnexion ou de perte de contexte). Le POST cancel permet à l&#x27;utilisateur d&#x27;annuler explicitement une session en cours.</p>
<p>Dans les deux cas, retournez l&#x27;état complet de la session avec le bon statut.</p>
<h3>Les webhooks</h3>
<p>L&#x27;API Checkout n&#x27;est pas unidirectionnelle. Vous devez aussi envoyer des webhooks vers OpenAI pour notifier des changements d&#x27;état de la commande. C&#x27;est comme ça que ChatGPT saura que la commande est confirmée, expédiée, livrée, ou annulée.</p>
<pre><code class="language-json">{
  &quot;type&quot;: &quot;order_updated&quot;,
  &quot;data&quot;: {
    &quot;type&quot;: &quot;order&quot;,
    &quot;checkout_session_id&quot;: &quot;checkout_session_abc123&quot;,
    &quot;permalink_url&quot;: &quot;https://yourshop.com/orders/order_789&quot;,
    &quot;status&quot;: &quot;shipped&quot;,
    &quot;refunds&quot;: []
  }
}
</code></pre>
<h2>Pilier 3 : Le paiement 💳</h2>
<p>Dernier pilier, et non des moindres : le paiement. C&#x27;est là que beaucoup se posent des questions légitimes sur la sécurité et la conformité PCI. OpenAI ne touche jamais directement à l&#x27;argent. Le protocole définit simplement comment transmettre de manière sécurisée les informations de paiement entre l&#x27;utilisateur, ChatGPT, et votre système. Vous restez en contrôle, et vous utilisez votre PSP habituel.</p>
<h3>Option 1 : Vous utilisez déjà Stripe</h3>
<p>C&#x27;est l&#x27;option la plus simple. Stripe a développé un mode &quot;agentic payments&quot; qui s&#x27;active en une ligne de code :</p>
<pre><code class="language-javascript">stripe.agentic_payments.enable()
</code></pre>
<p>Stripe gère alors la création et la transmission du token de paiement. Côté checkout, vous recevez un <code>payment_data.token</code> classique que vous chargez comme d&#x27;habitude.</p>
<h3>Option 2 : Vous utilisez un autre PSP</h3>
<p>Pas de Stripe dans votre stack ? Pas de panique, vous avez deux solutions.</p>
<p><strong>A. Via Stripe Shared Payment Token API</strong></p>
<p>Même si vous ne processez pas les paiements avec Stripe, vous pouvez utiliser leur API uniquement pour recevoir le token sécurisé. C&#x27;est une couche intermédiaire qui ne change rien à votre PSP principal (Adyen, Braintree, etc.). Vous recevez un token via Stripe, puis vous chargez le paiement avec votre PSP habituel.</p>
<p><strong>B. Implémentation directe (PSP ou PCI DSS Level 1 uniquement)</strong></p>
<p>Si vous êtes un PSP ou un merchant PCI DSS Level 1 avec votre propre vault, vous pouvez implémenter directement l&#x27;endpoint du protocole. Attention, cette option implique de manipuler directement les données de carte. Ne vous lancez là-dedans que si vous avez déjà l&#x27;infrastructure et la conformité PCI en place.</p>
<h2>Quelques points d&#x27;attention</h2>
<p>Avant de vous lancer, voici quelques limitations (à date) à garder en tête :</p>
<ul>
<li><strong>Géographie</strong> : USA uniquement pour l&#x27;instant</li>
<li><strong>Approbation</strong> : pas d&#x27;accès automatique, il faut postuler</li>
<li><strong>Single-item</strong> : pas de panier multi-produits (pour le moment)</li>
<li><strong>Pas de promo codes complexes</strong> : gardez la logique simple au début</li>
</ul>
<h2>Le mot de la fin</h2>
<p>Voilà, vous avez maintenant une vision complète de ce qu&#x27;implique l&#x27;implémentation de l&#x27;Agentic Commerce Protocol.</p>
<p><strong>Pour les développeurs</strong>, c&#x27;est une intégration REST classique. Rien de sorcier si vous avez déjà un système de checkout robuste. L&#x27;effort principal sera d&#x27;adapter votre logique métier (pricing, taxes, shipping) pour qu&#x27;elle soit exposable via une API.</p>
<p><strong>Pour les business</strong>, c&#x27;est un nouveau canal à tester. 700 millions d&#x27;utilisateurs de ChatGPT représentent une audience considérable. Même si l&#x27;accès est limité pour le moment, préparer votre infrastructure maintenant vous donnera un avantage quand les vannes s&#x27;ouvriront.</p>
<p>Chez Premier Octet, nous suivons de près cette évolution et commençons à accompagner nos premiers clients sur le sujet. Si vous envisagez d&#x27;implémenter l&#x27;Agentic Commerce Protocol et avez besoin d&#x27;accompagnement technique, n&#x27;hésitez pas à nous contacter. On serait ravis d&#x27;échanger sur votre projet !</p>
<p><strong>Ressources utiles :</strong></p>
<ul>
<li><a href="https://developers.openai.com/commerce">Documentation officielle OpenAI</a></li>
<li><a href="https://www.agenticcommerce.dev/">Documentation de l&#x27;Agentic Commerce Protocol</a></li>
<li><a href="https://docs.stripe.com/agentic-commerce">Documentation du Stripe</a></li>
</ul>
<p>👋</p>]]></content:encoded>
            <enclosure url="https://www.premieroctet.com/blog/ecommerce-conversationnel-avec-openai-et-stripe-guide-complet-de-l-agentic-commerce-protocol/illu.jpg" length="0" type="image/jpg"/>
        </item>
        <item>
            <title><![CDATA[Activity, le nouveau composant React]]></title>
            <link>https://www.premieroctet.com/blog/activity-nouveau-composant-react</link>
            <guid>https://www.premieroctet.com/blog/activity-nouveau-composant-react</guid>
            <pubDate>Tue, 30 Sep 2025 00:00:00 GMT</pubDate>
            <description><![CDATA[Fraichement sorti sur le canal canary de React, le composant Activity est une nouveauté ayant pour but de faciliter...]]></description>
            <content:encoded><![CDATA[<p>Fraichement sorti sur le canal canary de React, le composant Activity est une nouveauté ayant pour but de faciliter la gestion d&#x27;affichage conditionnel de composants, permettant de conserver leur état tout en les cachant visuellement. Découvrons ensemble son utilisation.</p>
<h2>Installation</h2>
<p>Créons un simple projet React avec Vite :</p>
<pre><code class="language-sh">bun create vite@latest
</code></pre>
<p>Nous devrons modifier le <code>package.json</code> afin de faire pointer les versions de React et React DOM vers la version canary.</p>
<h2>Cas d&#x27;utilisation</h2>
<p>Nous allons réaliser un cas d&#x27;utilisation qui peut s&#x27;avérer fréquent : la gestion d&#x27;un formulaire en plusieurs étapes.</p>
<h3>Utilisation basique</h3>
<p>Actuellement, l&#x27;utilisation la plus classique serait la suivante :</p>
<div><div><div value="App.tsx">App.tsx</div><div value="step1.tsx">step1.tsx</div><div value="step2.tsx">step2.tsx</div></div><div value="App.tsx"><pre><code class="language-tsx">function App() {
  const [step, setStep] = useState(1);

return (

&lt;div
  style={{
    margin: &#x27;1rem&#x27;,
    display: &#x27;flex&#x27;,
    alignItems: &#x27;center&#x27;,
    flex: 1,
    flexDirection: &#x27;column&#x27;,
    gap: &#x27;1rem&#x27;,
  }}
&gt;
  &lt;div style={{ display: &#x27;flex&#x27;, gap: &#x27;1rem&#x27; }}&gt;
    &lt;button type=&quot;button&quot; onClick={() =&gt; setStep(1)}&gt;
      Step 1
    &lt;/button&gt;
    &lt;button type=&quot;button&quot; onClick={() =&gt; setStep(2)}&gt;
      Step 2
    &lt;/button&gt;
  &lt;/div&gt;
  {step === 1 &amp;&amp; &lt;Step1 /&gt;}
  {step === 2 &amp;&amp; &lt;Step2 /&gt;}
&lt;/div&gt;
); }

</code></pre></div><div value="step1.tsx"><pre><code class="language-tsx">const Step1 = () =&gt; {
  const [name, setName] = useState(&quot;&quot;);

  return (
    &lt;input
      value={name}
      onChange={(e) =&gt; setName(e.target.value)}
      name=&quot;name&quot;
      placeholder=&quot;Name&quot;
    /&gt;
  );
};
</code></pre></div><div value="step2.tsx"><pre><code class="language-tsx">const Step2 = () =&gt; {
  const [address, setAddress] = useState(&quot;&quot;);

return (

&lt;input
  value={address}
  onChange={(e) =&gt; setAddress(e.target.value)}
  name=&quot;address&quot;
  placeholder=&quot;Address&quot;
/&gt;
); };

</code></pre></div></div>
<p>En l&#x27;état, ce bout de code est fonctionnel mais présente un inconvénient en terme d&#x27;UX : lorsque l&#x27;on remplit un champ puis que l&#x27;on change d&#x27;étape, l&#x27;état de ce champ est perdu. C&#x27;est logique : notre composant a été démonté. Pour palier à ça, on pourrait tout à fait modifier le code afin de cacher le composant, de sorte qu&#x27;il soit toujours présent dans l&#x27;arbre React.</p>
<pre><code class="language-tsx:App.tsx">function App() {
  const [step, setStep] = useState(1);

  return (
    &lt;div
      style={{
        margin: &quot;1rem&quot;,
        display: &quot;flex&quot;,
        alignItems: &quot;center&quot;,
        flex: 1,
        flexDirection: &quot;column&quot;,
        gap: &quot;1rem&quot;,
      }}
    &gt;
      &lt;div style={{ display: &quot;flex&quot;, gap: &quot;1rem&quot; }}&gt;
        &lt;button type=&quot;button&quot; onClick={() =&gt; setStep(1)}&gt;
          Step 1
        &lt;/button&gt;
        &lt;button type=&quot;button&quot; onClick={() =&gt; setStep(2)}&gt;
          Step 2
        &lt;/button&gt;
      &lt;/div&gt;
      &lt;div style={step === 2 ? { display: &quot;none&quot; } : {}}&gt;
        &lt;Step1 /&gt;
      &lt;/div&gt;
      &lt;div style={step === 1 ? { display: &quot;none&quot; } : {}}&gt;
        &lt;Step2 /&gt;
      &lt;/div&gt;
    &lt;/div&gt;
  );
}
</code></pre>
<p>C&#x27;est fonctionnel mais pas idéal, imaginons que nos composants doivent gérer des événements, ou du contenu interactif comme une vidéo, nous devrions quand même passer une prop afin de gérer leur activation / désactivation. De même, la présence d&#x27;un effect (<code>useEffect</code> ou <code>useLayoutEffect</code>) devrait avoir son déclenchement réglé selon cette prop. Bien que cela soit possible, cela rajouterait de la complexité à notre composant.</p>
<h3>Le composant Activity à la rescousse</h3>
<p>C&#x27;est dans ce cadre qu&#x27;intervient le composant <code>Activity</code>. Son rôle est simple : cacher le noeud du DOM avec un <code>display: none</code>, conserver l&#x27;état du noeud React, et n&#x27;exécuter les effects uniquement quand notre composant est visible. En quelque sorte, c&#x27;est comme si notre composant était partiellement monté et démonté.</p>
<pre><code class="language-tsx:App.tsx">function App() {
  const [step, setStep] = useState(1);

  return (
    &lt;div
      style={{
        margin: &quot;1rem&quot;,
        display: &quot;flex&quot;,
        alignItems: &quot;center&quot;,
        flex: 1,
        flexDirection: &quot;column&quot;,
        gap: &quot;1rem&quot;,
      }}
    &gt;
      &lt;div style={{ display: &quot;flex&quot;, gap: &quot;1rem&quot; }}&gt;
        &lt;button type=&quot;button&quot; onClick={() =&gt; setStep(1)}&gt;
          Step 1
        &lt;/button&gt;
        &lt;button type=&quot;button&quot; onClick={() =&gt; setStep(2)}&gt;
          Step 2
        &lt;/button&gt;
      &lt;/div&gt;
      &lt;Activity mode={step === 1 ? &quot;visible&quot; : &quot;hidden&quot;}&gt;
        &lt;Step1 /&gt;
      &lt;/Activity&gt;
      &lt;Activity mode={step === 2 ? &quot;visible&quot; : &quot;hidden&quot;}&gt;
        &lt;Step2 /&gt;
      &lt;/Activity&gt;
    &lt;/div&gt;
  );
}
</code></pre>
<p>La saisie de texte est maintenant conservée entre chaque changement d&#x27;étape. En inspectant le DOM, on peut voir qu&#x27;un style <code>display: none</code> est appliqué à notre input caché.</p>
<p>Pour aller plus loin, on peut observer le comportement des effects, à l&#x27;aide de simples <code>console.log</code>.</p>
<pre><code class="language-tsx:step2.tsx">const Step2 = () =&gt; {
  const [address, setAddress] = useState(&quot;&quot;);

  useEffect(() =&gt; {
    console.log(&quot;Step2 mounted&quot;);

    return () =&gt; {
      console.log(&quot;Step2 unmounted&quot;);
    };
  }, []);

  useLayoutEffect(() =&gt; {
    console.log(&quot;Step2 layout mounted&quot;);

    return () =&gt; {
      console.log(&quot;Step2 layout unmounted&quot;);
    };
  }, []);

  return (
    &lt;input
      value={address}
      onChange={(e) =&gt; setAddress(e.target.value)}
      name=&quot;address&quot;
      placeholder=&quot;Address&quot;
    /&gt;
  );
};
</code></pre>
<p>En rafraichissant la page, on peut voir qu&#x27;aucun des logs ne s&#x27;affiche. Pourtant, notre input est bien dans le DOM. En cliquant sur notre étape 2, nos effects sont bien déclenchés, et les fonctions de cleanup sont exécutées si l&#x27;on revient à l&#x27;étape 1.</p>
<h3>Pré-rendu et données distantes</h3>
<p>Afin de récupérer des données distantes, un cas d&#x27;utilisation courant serait d&#x27;effectuer une requête dans un <code>useEffect</code>. Or on a vu précédemment que les effects ne sont pas exécutés dans le cadre de l&#x27;utilisation d&#x27;<code>Activity</code>. Pour palier à cela, nous allons utiliser le hook <code>use</code>.</p>
<pre><code class="language-tsx:step2.tsx">const fetchAddress = new Promise&lt;string&gt;((resolve) =&gt; {
  resolve(&quot;Some street name&quot;);
});

const Step2 = () =&gt; {
  const addressData = use(fetchAddress);
  const [address, setAddress] = useState(addressData);

  return (
    &lt;input
      value={address}
      onChange={(e) =&gt; setAddress(e.target.value)}
      name=&quot;address&quot;
      placeholder=&quot;Address&quot;
    /&gt;
  );
};
</code></pre>
<p>Maintenant, au chargement de la page, notre promesse <code>fetchAddress</code> est exécutée dès lors que <code>Step2</code> est instancié, et on retrouve la valeur résolue affichée dans notre input.</p>

<h2>Conclusion</h2>
<p>Ce tout nouveau composant <code>Activity</code> va simplifier certains cas complexes que l&#x27;on pouvait rencontrer jusqu&#x27;à présent dans nos applications. Son utilisation est très intuitive, mais nécessitera toutefois un peu de refactorisation dans les projets existants, notamment par rapport à la modification du comportement des effects.</p>
<p>Une idée de projet React, besoin d&#x27;accompagnement ? N&#x27;hésitez pas à <a href="https://www.premieroctet.com/contact">nous contacter</a> !</p>
<p>Ressources:</p>
<ul>
<li><a href="https://react.dev/reference/react/Activity">Documentation officielle Activity</a></li>
</ul>]]></content:encoded>
            <enclosure url="https://www.premieroctet.com/blog/activity-nouveau-composant-react/illu.png" length="0" type="image/png"/>
        </item>
        <item>
            <title><![CDATA[Comment utiliser Rive app pour animer vos interfaces Web et mobiles]]></title>
            <link>https://www.premieroctet.com/blog/comment-utiliser-rive-app-pour-animer-vos-interfaces-web-et-mobiles</link>
            <guid>https://www.premieroctet.com/blog/comment-utiliser-rive-app-pour-animer-vos-interfaces-web-et-mobiles</guid>
            <pubDate>Tue, 23 Sep 2025 00:00:00 GMT</pubDate>
            <description><![CDATA[Découvrez comment créer des animations interactives avec Rive app et les intégrer dans vos applications React pour des interfaces dynamiques et engageantes.]]></description>
            <content:encoded><![CDATA[<p>Si vous avez déjà joué avec <a href="https://www.premieroctet.com/estimateur-projet-web-mobile">notre estimateur web et mobile</a>, vous avez peut-être remarqué que l’illustration à l’écran évolue en fonction de vos choix. Une option cochée, et hop ! un petit élément s’affiche avec une animation fluide. Ce petit tour de magie, on le doit à Rive app.</p>
<div style="margin-top:20px"><video class="b-lazy b-loaded" muted="" controls="" title="Timeline animation"><source src="https://www.premieroctet.com/blog/comment-utiliser-rive-app-pour-animer-vos-interfaces-web-et-mobiles/demo.mp4" type="video/mp4"/></video></div>
<p>Dans cet article, je vous propose de voir comment mettre en place ces animations — et surtout, pourquoi on a choisi cet outil plutôt qu’un autre.</p>
<h2>Qu&#x27;est-ce que Rive App ?</h2>
<p>Rive est un outil de création d’animations vectorielles interactives. Là où d’autres génèrent des vidéos ou des gifs, Rive produit des fichiers <code>.riv</code>, très légers, qui peuvent réagir aux actions de l’utilisateur.</p>
<h3>Les avantages de Rive App</h3>
<p>Rive App a plusieurs avantages :</p>
<ul>
<li><strong>Animations interactives</strong> : vos animations peuvent changer d&#x27;état selon les interactions utilisateur (clics, survols, données dynamiques)</li>
<li><strong>Fichiers ultra-légers</strong> : les animations Rive sont des fichiers vectoriels optimisés, bien plus petits que des vidéos équivalentes ou des gifs</li>
<li><strong>Multi-plateforme</strong> : une seule animation fonctionne sur Web ou mobile grâce aux différents runtimes (et bien entendu React / React Native)</li>
<li><strong>État dynamique</strong> : possibilité de créer des state machines complexes avec conditions et transitions et faire du data-binding</li>
</ul>
<h3>Pourquoi choisir Rive plutôt que Lottie ou des animations CSS ?</h3>
<p>Rive se distingue par sa capacité à créer des animations <strong>véritablement interactives</strong>. Là où Lottie se limite à des animations linéaires et le CSS pour des transitions simples, Rive permet de créer des expériences où l&#x27;animation évolue en fonction du contexte applicatif.</p>
<p>Pour les anciens, Rive se rapproche de Flash, qui était un outil de création d’animations vectorielles interactives.</p>
<h2>Deux modes, une interface</h2>
<p>L’interface de Rive est découpée en deux parties : Design et Animation. Une pour préparer les assets, l’autre pour leur donner vie !</p>
<p>Je vous propose de détailler l&#x27;animation d&#x27;un élément de notre estimateur web et mobile.</p>
<h3>Étape 1 : préparer le terrain</h3>
<p>On commence par importer notre illustration (préparée dans Figma), et on isole chaque élément sur un calque séparé. Cette étape est un peu fastidieuse mais cruciale. Un calque = un élément animable. Sans ça, on se prend vite les pieds dans le tapis.</p>
<p>Voici mon illustration préparée avec chaque élément isolé, visible dans le panel de gauche <code>Hierarchy</code> :</p>
<p><img src="https://www.premieroctet.com/blog/comment-utiliser-rive-app-pour-animer-vos-interfaces-web-et-mobiles/rive-app-ui-design.png" alt="Interface de design Rive app avec panel Hierarchy et découpage des éléments"/></p>
<h3>Étape 2 : mode animation</h3>
<p>Une fois l’illustration prête, on passe en mode animation grâce au switch en haut à droite de l&#x27;interface.</p>
<p>C’est là que les timelines, state machines et interpolations entrent en scène. Les animations avec Rive app se basent sur des timelines d&#x27;interpolation de propriétés (position, opacité, coordonnées, etc.) assez semblables aux interfaces de motion design comme After Effects.</p>
<p>Prenons l’exemple d’un élément que l’on veut faire apparaître dynamiquement : ici, une paire de guillemets en haut à droite de l’illustration.</p>
<p>On commence par créer un input de type boolean dans la state machine : <code>has_blog</code>. Il est à <code>false</code> par défaut, et c’est notre code React que nous écrirons plus tard qui viendra le passer à <code>true</code> :</p>
<p><img src="https://www.premieroctet.com/blog/comment-utiliser-rive-app-pour-animer-vos-interfaces-web-et-mobiles/rive-app-panel-animation.png" alt="Interface d&#x27;animation Rive app avec panel Inputs et création d&#x27;un input de type boolean"/></p>
<p>On peut maintenant passer à l&#x27;animation !</p>
<p>On crée une nouvelle timeline dédiée à cette animation sur laquelle on va interpoler deux propriétés : position et opacité.</p>
<p>Ce n&#x27;est pas du Miyazaki mais suffisant pour donner un effet vivant à notre élément :</p>
<div style="margin-top:20px"><video class="b-lazy b-loaded" autoplay="" loop="" muted="" controls="" title="Timeline animation"><source src="https://www.premieroctet.com/blog/comment-utiliser-rive-app-pour-animer-vos-interfaces-web-et-mobiles/rive-app-timeline-animation.mp4" type="video/mp4"/></video></div>
<h3>Mettre en place la state machine</h3>
<p>Une fois l&#x27;animation créée, on peut maintenant créer notre state machine spécifique à cette animation.</p>
<p>Les states machines Rive permettent de gérer l&#x27;état de l&#x27;animation en fonction de conditions. Ici, on va créer deux états : l&#x27;animation qui se joue et l&#x27;animation qui disparaît (si l&#x27;utilisateur décoche l&#x27;option).</p>
<p><img src="https://www.premieroctet.com/blog/comment-utiliser-rive-app-pour-animer-vos-interfaces-web-et-mobiles/rive-app-state-machine-step-1.png" alt="State machine étape 1"/></p>
<p>Pour l&#x27;animation de disparition, j&#x27;ai utilisé une astuce qui permet de jouer l&#x27;animation dans le sens inverse (notez le speed -1 dans le panel en bas à droite) :</p>
<p><img src="https://www.premieroctet.com/blog/comment-utiliser-rive-app-pour-animer-vos-interfaces-web-et-mobiles/rive.png" alt="State machine étape 2"/></p>
<p>Il ne reste plus qu&#x27;à ajouter mes conditions qui permettent de passer d&#x27;un état à l&#x27;autre simplement basé sur ma condition <code>has_blog</code> :</p>
<p><img src="https://www.premieroctet.com/blog/comment-utiliser-rive-app-pour-animer-vos-interfaces-web-et-mobiles/rive-app-state-machine-step-2.png" alt="State machine étape 2"/></p>
<p>L&#x27;animation est maintenant prête ! Chaque animation de notre estimateur repose sur ce même pattern. C’est un peu répétitif, mais le résultat final vaut l’investissement.</p>
<h2>Intégrer votre animation Rive dans React</h2>
<p>On peut maintenant intégrer notre animation dans notre code React. Pour cela on peut utiliser <a href="https://rive.app/docs/runtimes/react/react">le runtime React</a> officiel de Rive.</p>
<p>Avant toute chose, il vous faut exporter votre animation au format <code>.riv</code> afin de le passer au composant <code>&lt;Rive/&gt;</code> :</p>
<pre><code class="language-jsx">import { useRive, useStateMachineInput } from &#x27;@rive-app/react-canvas&#x27;

export const App = () =&gt; {
  // Configuration de l&#x27;animation Rive
  const { rive, RiveComponent } = useRive({
    src: &#x27;/animations/estimator.riv&#x27;, // Chemin vers votre fichier .riv
    artboard: &#x27;web&#x27;, // Nom de l&#x27;artboard à utiliser
    stateMachines: &#x27;state_machine&#x27;, // Nom de la state machine
    autoplay: true, // Démarre automatiquement l&#x27;animation
  })

  // Récupération de l&#x27;input de la state machine
  const hasBlog = useStateMachineInput(rive, &#x27;state_machine&#x27;, &#x27;has_blog&#x27;)

  return (
    &lt;&gt;
      &lt;RiveComponent /&gt;
      &lt;button onClick={() =&gt; (hasBlog.value = true)}&gt;Afficher l&#x27;animation Blog&lt;/button&gt;
    &lt;/&gt;
  )
}
</code></pre>
<p>Voici une petite explication du code :</p>
<ul>
<li>
<p><strong><code>useRive</code></strong> : ce hook configure et initialise votre animation Rive. Il retourne l&#x27;instance <code>rive</code> et le composant <code>RiveComponent</code> à afficher.</p>
</li>
<li>
<p><strong><code>useStateMachineInput</code></strong> : ce hook permet de récupérer et manipuler les inputs de votre state machine. Ici, on récupère l&#x27;input <code>has_blog</code> qu&#x27;on avait configuré dans Rive app.</p>
</li>
<li>
<p><strong>Interaction</strong> : en cliquant sur le bouton, on modifie la valeur de <code>hasBlog.value</code> à <code>true</code>, ce qui déclenche l&#x27;animation dans Rive.</p>
</li>
</ul>
<p>Votre animation est ainsi prête à être utilisée dans votre application !</p>
<h2>En résumé</h2>
<p>Rive permettet d&#x27;apporter un supplément d&#x27;âme à vos projets, sans sacrifier la performance. Son modèle basé sur des state machines permet de créer des comportements fins, réactifs et parfaitement intégrés à nos apps.</p>
<p>L&#x27;intégration avec React est fluide, sans friction. Et surtout, elle garde les responsabilités bien séparées : Rive pour l’animation, React pour le déclenchement.</p>
<p>C’est un outil qu’on adopte rapidement une fois qu’on a mis les mains dedans. Mon conseil : commencez simple et explorez petit à petit.</p>
<p>Voici quelques use cases concrets :</p>
<ul>
<li>Animer des éléments d&#x27;une interface comme notre estimateur Web et mobile</li>
<li>Intégrer des animations dans une application React Native (écran de onboarding, etc.)</li>
<li>Créer des apps à part entière (jeux, etc.)</li>
<li>Créer des layouts interactifs pour des streaming videos (via OBS, etc.)</li>
</ul>
<p>Si vous avez des projets qui nécessitent des animations interactives, n&#x27;hésitez pas à <a href="https://www.premieroctet.com/contact">nous contacter</a>, nous serons ravis de vous accompagner dans la mise en place de <a href="https://rive.app/">Rive app</a>.</p>
<p>Enfin, vous pouvez jouer avec <a href="https://www.premieroctet.com/estimateur-projet-web-mobile">nos estimateurs Web et mobile ici</a>.</p>
<p>Ressources utiles :</p>
<ul>
<li><a href="https://rive.app/docs/getting-started/introduction">Documentation officielle Rive app</a></li>
<li><a href="https://rive.app/docs/runtimes/react/react">Runtime React</a></li>
</ul>
<p>👋</p>]]></content:encoded>
            <enclosure url="https://www.premieroctet.com/blog/comment-utiliser-rive-app-pour-animer-vos-interfaces-web-et-mobiles/illu.png" length="0" type="image/png"/>
        </item>
        <item>
            <title><![CDATA[Stylisez votre application React Native avec Unistyles]]></title>
            <link>https://www.premieroctet.com/blog/stylisez-votre-application-react-native-avec-unistyles</link>
            <guid>https://www.premieroctet.com/blog/stylisez-votre-application-react-native-avec-unistyles</guid>
            <pubDate>Tue, 16 Sep 2025 00:00:00 GMT</pubDate>
            <description><![CDATA[Alors que la nouvelle architecture de React Native est maintenant dans un état stable, la quête à la performance elle n'est jamais terminée...]]></description>
            <content:encoded><![CDATA[<p>Alors que la <a href="https://reactnative.dev/architecture/landing-page">nouvelle architecture de React Native</a> est maintenant dans un état stable, la quête à la performance elle n&#x27;est jamais terminée. Dans ce cadre, utiliser LA librairie de style pour notre application est n&#x27;est pas toujours évident. En effet celle-ci doit allier performance, flexibilité et simplicité d&#x27;utilisation. Nous avons exploré dans un précédent article <a href="https://www.premieroctet.com/blog/decouverte-tamagui">Tamagui</a> qui offre une certaine simplicité d&#x27;utilisation, mais qui est assez fastidieux à configurer lorsque l&#x27;on souhaite obtenir un rendu plus personnalisé.</p>
<p>Plus récemment, <a href="https://www.nativewind.dev">NativeWind</a> a prit aussi beaucoup en popularité, profitant d&#x27;une certaine standardisation de l&#x27;utilisation de <a href="https://tailwindcss.com/">Tailwind</a> côté web. Simple à configurer, avec une API quasi identique à celle de Tailwind, il est assez simple pour un développeur Web de se lancer dans la création d&#x27;une application React Native avec NativeWind. Néanmoins, son fonctionnement interne n&#x27;est pas toujours évident à comprendre et demande parfois d&#x27;être assez verbeux quand il s&#x27;agit de créer des composants très génériques (via l&#x27;utilisation de <a href="https://www.nativewind.dev/docs/api/remap-props"><code>remapProps</code></a> ou <a href="https://www.nativewind.dev/docs/api/css-interop"><code>cssInterop</code></a> par exemple).</p>
<p>C&#x27;est dans ce contexte de course à la performance et à la meilleure DX qu&#x27;Unistyles a sorti sa version 3, dont nous allons dans cet article explorer les fonctionnalités et les avantages de cette librairie.</p>
<h2>Comment ça marche ?</h2>
<p>Unistyles a pour vocation de fournir une API très proche de l&#x27;API <a href="https://reactnative.dev/docs/stylesheet">StyleSheet</a> de React Native, tout en y ajoutant des fonctionnalités supplémentaires comme la gestion de thème, le support des breakpoints, l&#x27;ajout de variants. Pour aller plus loin, Unistyles ajoute un aspect performance en gérant les styles directement au sein de la partie C++ de l&#x27;application. Concrètement, cela permet d&#x27;associer un style à un noeud natif de notre arbre de composants, et de faire en sorte que ce noeud réagisse aux changements de style qui peuvent se produire (changement de thème, propriété dépendante du viewport, etc.), le tout sans provoquer de re-rendu de notre arbre de composants côté JavaScript. Toutes ces optimisations sont orchestrées par un <a href="https://www.unistyl.es/v3/other/babel-plugin">plugin Babel</a>.</p>
<h2>Utilisation</h2>
<p>Explorons Unistyles en créant un projet Expo. Nous utiliserons le template &quot;Blank&quot; :</p>
<pre><code class="language-sh">bun create expo-app demo-unistyles -t
cd demo-unistyles
npx expo install react-native-unistyles react-native-nitro-modules react-native-reanimated react-native-edge-to-edge
</code></pre>
<p>Ajoutons le plugin Babel :</p>
<pre><code class="language-sh">npx expo customize babel.config.js
</code></pre>
<pre><code class="language-js:babel.config.js">module.exports = function (api) {
  api.cache(true);
  return {
    presets: [&quot;babel-preset-expo&quot;],
    plugins: [
      [
        &quot;react-native-unistyles/plugin&quot;,
        {
          root: &quot;src&quot;, // Dossier racine contenant les composants stylisés
        },
      ],
    ],
  };
};
</code></pre>
<p>Créons maintenant un style très simple dans le composant <code>App</code> :</p>
<pre><code class="language-js:src/app.tsx">import { StyleSheet } from &#x27;react-native-unistyles&#x27;;
import { View } from &#x27;react-native&#x27;;

const styles = StyleSheet.create({
  container: {
    flex: 1,
    justifyContent: &#x27;center&#x27;,
    alignItems: &#x27;center&#x27;,
    backgroundColor: &#x27;#FFFFFF&#x27;,
  },
});

const App = () =&gt; {
  return &lt;View style={styles.container} /&gt;
}

export default App;
</code></pre>
<p>On constate finalement que, en l&#x27;état, l&#x27;API de Unistyles offre une parité 1:1 avec l&#x27;API <code>StyleSheet</code> de React Native, rendant ainsi une migration vers Unistyles très simple.</p>
<h3>Theming</h3>
<p>Nous allons maintenant utiliser ce qui est un atout d&#x27;Unistyles par rapport à <code>StyleSheet</code> : la possibilité de définir des thèmes.</p>
<p>Créons un fichier <code>src/themes.ts</code> :</p>
<pre><code class="language-ts:src/themes.ts">import { StyleSheet } from &quot;react-native-unistyles&quot;;

// Quelques utilitaires
const spacing = (size: number) =&gt; size * 4;
const radius = (size: number) =&gt; size * 4;

const utils = {
  spacing,
  radius,
};

export const light = {
  colors: {
    primary: &quot;#007AFF&quot;,
    red: &quot;#ed3e3e&quot;,
    background: &quot;#ffffff&quot;,
  },
  utils,
};

export const dark = {
  colors: {
    primary: &quot;#4da6ff&quot;,
    red: &quot;#fa645c&quot;,
    background: &quot;#3E3E3E&quot;,
  },
  utils,
};

StyleSheet.configure({
  themes: {
    light,
    dark,
  },
  settings: {
    /**
     * L&#x27;utilisation de thèmes nommés &quot;dark&quot; et &quot;light&quot; permet de les utiliser en fonction
     * du thème utilisé par le système d&#x27;exploitation. Pour cela, il faut s&#x27;assurer que
     * la propriété `userInterfaceStyle` de la configuration Expo soit définie en &quot;automatic&quot;.
     */
    adaptiveThemes: true,
  },
});

type AppThemes = {
  light: typeof light;
  dark: typeof dark;
};

declare module &quot;react-native-unistyles&quot; {
  export interface UnistylesThemes extends AppThemes {}
}
</code></pre>
<p>Nous avons ici défini 2 thèmes de couleur différent, chacun destiné à s&#x27;adapter selon le thème du système d&#x27;exploitation. Il est évidemment possible de définir d&#x27;autres thèmes avec des noms différents.
Pour que ces thèmes soient bien pris en compte avant le premier rendu, il est impératif d&#x27;importer ce fichier dans notre <code>index.ts</code> :</p>
<pre><code class="language-js:index.ts">import { registerRootComponent } from &quot;expo&quot;;
import &quot;./src/theme&quot;;
import App from &quot;./src/app&quot;;

registerRootComponent(App);
</code></pre>
<p>Faisons maintenant usage de ces thèmes. Pour ce faire, il va falloir transformer légèrement notre fonction <code>StyleSheet.create</code>. Celle-ci prendra maintenant en paramètre une fonction recevant 2 arguments : le thème actuel et des variables de styling liées à l&#x27;appareil (nous aborderons cela plus tard).</p>
<pre><code class="language-tsx:src/app.tsx">import { StyleSheet } from &#x27;react-native-unistyles&#x27;;
import { View } from &#x27;react-native&#x27;;

const styles = StyleSheet.create((theme, rt) =&gt; ({
  container: {
    flex: 1,
    justifyContent: &#x27;center&#x27;,
    alignItems: &#x27;center&#x27;,
    backgroundColor: theme.colors.background,
    padding: theme.utils.spacing(4)
  },
}));

const App = () =&gt; {
  return &lt;View style={styles.container} /&gt;
}

export default App;
</code></pre>
<p>Si nous alternons entre le light et dark mode sur notre appareil, nous verrons la couleur de fond changer, le tout sans que notre arbre de composants React ne soit re-rendu !</p>
<h3>Responsive</h3>
<p>Il est possible de définir des styles selon la taille ou l&#x27;orientation de l&#x27;appareil. Par défaut, Unistyles fournit la possibilité de fournir des styles selon des breakpoints liés à l&#x27;orientation, mais il est tout à fait possible de définir <a href="https://www.unistyl.es/v3/references/breakpoints">nos propres breakpoints</a> liés à la taille de l&#x27;écran. Essayons par exemple de changer la couleur de fond en fonction de l&#x27;orientation :</p>
<pre><code class="language-tsx:src/app.tsx">import { StyleSheet } from &#x27;react-native-unistyles&#x27;;
import { View } from &#x27;react-native&#x27;;

const styles = StyleSheet.create((theme, rt) =&gt; ({
  container: {
    flex: 1,
    justifyContent: &#x27;center&#x27;,
    alignItems: &#x27;center&#x27;,
    backgroundColor: {
      portrait: theme.colors.background,
      landscape: theme.colors.primary,
    },
    padding: theme.utils.spacing(4)
  },
}));

const App = () =&gt; {
  return &lt;View style={styles.container} /&gt;
}

export default App;
</code></pre>
<p>Maintenant, lorsque nous changeons l&#x27;orientation de notre appareil, la couleur de fond change, toujours sans provoquer de re-rendu !</p>
<h3>Runtime Unistyles</h3>
<p>Évoquons maintenant le sujet du runtime que l&#x27;on a vu précédemment. Unistyles expose des variables de runtime, qui sont des valeurs purement liées à l&#x27;appareil et qui sont accessibles à tout moment. Il existe 2 types de runtime :</p>
<ul>
<li><a href="https://www.unistyl.es/v3/references/unistyles-runtime">le runtime global</a>, exposant des variables accessibles à tout moment, mais aussi des fonctions permettant d&#x27;altérer certains aspects comme le thème utilisé, le contenu du thème, la couleur de fond de notre vue native, etc.</li>
<li><a href="https://www.unistyl.es/v3/references/mini-runtime">le runtime de style</a>, appelé mini runtime, qui lui aussi expose des variables similaires à celles du runtime global, mais qui ont pour but d&#x27;être utilisées dans les styles. Ainsi, nos styles seront réactifs aux changements de valeurs de ces variables.</li>
</ul>
<p>Faisons usage du mini runtime pour ajouter un espacement en haut de notre vue en fonction de la safe area :</p>
<pre><code class="language-tsx:src/app.tsx">import { StyleSheet } from &#x27;react-native-unistyles&#x27;;
import { View } from &#x27;react-native&#x27;;

const styles = StyleSheet.create((theme, rt) =&gt; ({
  container: {
    flex: 1,
    justifyContent: &#x27;center&#x27;,
    alignItems: &#x27;center&#x27;,
    backgroundColor: {
      portrait: theme.colors.background,
      landscape: theme.colors.primary,
    },
    padding: theme.utils.spacing(4),
    paddingTop: rt.insets.top,
  },
}));

const App = () =&gt; {
  return &lt;View style={styles.container} /&gt;
}

export default App;
</code></pre>
<h3>Variants</h3>
<p>À la manière de Tamagui et de bien d&#x27;autres librairies de style, Unistyles permet d&#x27;ajouter des variants pour un style précis. Un cas d&#x27;utilisation parfait pour cela est la création d&#x27;un bouton. Pour un aspect plus joli, nous allons même nous permettre d&#x27;animer la couleur du bouton quand un changement de couleur est détecté.</p>
<pre><code class="language-tsx:src/button.tsx">import { PropsWithChildren } from &quot;react&quot;;
import {
  Pressable,
  type PressableProps,
  StyleProp,
  Text,
  ViewStyle,
} from &quot;react-native&quot;;
import Animated, {
  useAnimatedStyle,
  withTiming,
} from &quot;react-native-reanimated&quot;;
import { StyleSheet, type UnistylesVariants } from &quot;react-native-unistyles&quot;;
import { useAnimatedVariantColor } from &quot;react-native-unistyles/reanimated&quot;;

type Props = UnistylesVariants&lt;typeof styles&gt; &amp;
  Omit&lt;PressableProps, &quot;style&quot;&gt; &amp; {
    label: string;
    style?: StyleProp&lt;ViewStyle&gt;;
  };

const Button = ({
  type = &quot;primary&quot;,
  style,
  label,
  children,
  ...props
}: Props) =&gt; {
  // Activation du variant pour le composant Button
  styles.useVariants({ type });
  // Récupération de la couleur de fond sous forme de shared value
  const color = useAnimatedVariantColor(styles.container, &quot;backgroundColor&quot;);
  // Animation de la couleur de fond
  const animatedStyle = useAnimatedStyle(() =&gt; {
    return {
      backgroundColor: withTiming(color.value, { duration: 500 }),
    };
  });

  return (
    &lt;Pressable {...props}&gt;
      &lt;Animated.View style={[styles.container, animatedStyle, style]}&gt;
        &lt;ButtonText type={type}&gt;{label}&lt;/ButtonText&gt;
      &lt;/Animated.View&gt;
    &lt;/Pressable&gt;
  );
};

const ButtonText = ({
  children,
  type,
}: PropsWithChildren&lt;UnistylesVariants&lt;typeof styles&gt;&gt;) =&gt; {
  // Activation du variant pour le composant ButtonText
  styles.useVariants({ type });

  return &lt;Text style={styles.text}&gt;{children}&lt;/Text&gt;;
};

export default Button;

const styles = StyleSheet.create((theme, rt) =&gt; ({
  container: {
    borderRadius: theme.utils.radius(2),
    flexDirection: &quot;row&quot;,
    paddingHorizontal: theme.utils.spacing(4),
    paddingVertical: theme.utils.spacing(3),

    variants: {
      type: {
        primary: {
          backgroundColor: theme.colors.primary,
        },
        destructive: {
          backgroundColor: theme.colors.red,
        },
      },
    },
  },
  text: {
    fontSize: 14,
    variants: {
      type: {
        primary: {
          color: &quot;white&quot;,
        },
        destructive: {
          color: &quot;white&quot;,
        },
      },
    },
  },
}));
</code></pre>
<p>Nous avons défini ici 2 variants pour nos boutons. Ces variants peuvent être passé en props de notre composant <code>Button</code>.</p>
<pre><code class="language-tsx:src/app.tsx">import { StyleSheet } from &#x27;react-native-unistyles&#x27;;
import { View } from &#x27;react-native&#x27;;
import Button from &#x27;./button&#x27;;

const styles = StyleSheet.create((theme, rt) =&gt; ({
  container: {
    flex: 1,
    justifyContent: &#x27;center&#x27;,
    alignItems: &#x27;center&#x27;,
    backgroundColor: {
      portrait: theme.colors.background,
      landscape: theme.colors.primary,
    },
    padding: theme.utils.spacing(4),
    paddingTop: rt.insets.top,
    gap: theme.utils.spacing(4),
  },
}));

const App = () =&gt; {
  return (
    &lt;View style={styles.container}&gt;
      &lt;Button type=&quot;primary&quot; label=&quot;Primary Button&quot; /&gt;
      &lt;Button type=&quot;destructive&quot; label=&quot;Destructive Button&quot; /&gt;
    &lt;/View&gt;
  )
}

export default App;
</code></pre>
<p>Profitons-en pour faire usage du runtime global. Nous allons faire en sorte que nos boutons changent le thème de couleur utilisé.</p>
<pre><code class="language-tsx:src/app.tsx">import { StyleSheet, UnistylesRuntime } from &#x27;react-native-unistyles&#x27;;
import { View } from &#x27;react-native&#x27;;
import Button from &#x27;./button&#x27;;

const styles = StyleSheet.create((theme, rt) =&gt; ({
  container: {
    flex: 1,
    justifyContent: &#x27;center&#x27;,
    alignItems: &#x27;center&#x27;,
    backgroundColor: {
      portrait: theme.colors.background,
      landscape: theme.colors.primary,
    },
    padding: theme.utils.spacing(4),
    paddingTop: rt.insets.top,
    gap: theme.utils.spacing(4),
  },
}));

const App = () =&gt; {
  return (
    &lt;View style={styles.container}&gt;
      &lt;Button type=&quot;primary&quot; label=&quot;Light mode&quot; onPress={() =&gt; UnistylesRuntime.setTheme(&#x27;light&#x27;)} /&gt;
      &lt;Button type=&quot;destructive&quot; label=&quot;Dark mode&quot; onPress={() =&gt; UnistylesRuntime.setTheme(&#x27;dark&#x27;)} /&gt;
    &lt;/View&gt;
  )
}

export default App;
</code></pre>
<p>Maintenant au clic sur les boutons, leur couleur de fond change, encore une fois sans provoquer de re-rendu.</p>
<p>En plus des variants, Unistyles offre la possibilité d&#x27;ajouter des styles selon les valeurs de plusieurs variants : <a href="https://www.unistyl.es/v3/references/compound-variants">les variants composés</a>.</p>
<h2>Conclusion</h2>
<p>Unistyles est une alternative très intéressante aux solutions existantes, de part son aspect performance, sa simplicité d&#x27;utilisation et ses similitudes avec l&#x27;API StyleSheet. À noter qu&#x27;un <a href="https://www.unistyl.es/v3/references/web-styles">support Web</a> est lui aussi disponible. Que choisir entre Unistyles et Tamagui ? Le choix est discutable mais de part sa facilité de configuration, Unistyles me semble être un bon choix. Si toutefois vous êtes un grand fan de Tailwind, le créateur d&#x27;Unistyles a créé <a href="https://uniwind.dev/">Uniwind</a> (pas encore sorti à l&#x27;heure de l&#x27;écriture de cet article), ayant pour but de combiner les performances de Unistyles avec les avantages de Tailwind.</p>
<p>Des idées de projet en React Native, une envie de migrer votre application vers Unistyles ? N&#x27;hésitez pas à <a href="https://www.premieroctet.com/contact">nous contacter</a> !</p>]]></content:encoded>
            <enclosure url="https://www.premieroctet.com/blog/stylisez-votre-application-react-native-avec-unistyles/illu.png" length="0" type="image/png"/>
        </item>
        <item>
            <title><![CDATA[Intégration d'un MCP dans une application React]]></title>
            <link>https://www.premieroctet.com/blog/integration-mcp-dans-une-app-react</link>
            <guid>https://www.premieroctet.com/blog/integration-mcp-dans-une-app-react</guid>
            <pubDate>Tue, 26 Aug 2025 00:00:00 GMT</pubDate>
            <description><![CDATA[La démocratisation de l'IA via les LLM a rendu l'accès à tout type d'information extrêmement facile et rapide...]]></description>
            <content:encoded><![CDATA[<p>La démocratisation de l&#x27;IA via les LLM a rendu l&#x27;accès à tout type d&#x27;information extrêmement facile et rapide, sans nécessiter de connaissances techniques avancées. Cependant dans certains cas, les modèles n&#x27;ont tout simplement pas le contexte nécessaire pour répondre à des demandes spécifiques. Par exemple, répondre à une question liée à de la facturation liée à un compte Stripe est impossible sans devoir développer un assistant spécifique, via l&#x27;intégration de <a href="https://ai-sdk.dev/docs/foundations/tools#tools">tools</a>. Pour palier à cela, <a href="https://www.anthropic.com/">Anthropic</a>, à l&#x27;origine de <a href="https://www.anthropic.com/claude">Claude</a>,
a mis en place un protocole de communication permettant de fournir des informations contextuelles aux modèles sans nécessiter de développements spécifiques à un contexte particulier.</p>
<h2>Model Context Protocol (MCP)</h2>
<p><a href="https://modelcontextprotocol.io/">Model Context Protocol (MCP)</a> est un standard de communication permettant de fournir un contexte à un LLM. En tant que développeur, vous êtes probablement familier avec MCP si vous utilisez un IDE supportant les agents IA (Cursor, Zed, etc.). Un serveur implémentant MCP peut fournir plusieurs types de données :</p>
<ul>
<li><a href="https://modelcontextprotocol.io/docs/concepts/tools">un ensemble de tools à exécuter</a></li>
<li><a href="https://modelcontextprotocol.io/docs/concepts/prompts">des prompts prédéfinis</a></li>
<li><a href="https://modelcontextprotocol.io/docs/concepts/resources">des données précises venant d&#x27;une source externe (BDD, API, etc.)</a></li>
</ul>
<p>En réalité, l&#x27;usage le plus courant est d&#x27;utiliser les tools exposés par le serveur MCP.</p>
<p>Ce serveur peut ensuite être exposé soit via une URL, soit un exécutable.</p>
<h2>Implémentation</h2>
<p>Nous allons créer une petite application Next.js utilisant un MCP Postgres. Le but est de pouvoir répondre à une requête dans un langage naturel puis d&#x27;afficher les informations issues de la base de données.</p>
<pre><code class="language-sh">bun create next-app
</code></pre>
<p>Installons les dépendances nécessaires :</p>
<pre><code class="language-sh">bun add ai @ai-sdk/anthropic @modelcontextprotocol/sdk pg react-markdown
</code></pre>
<h3>Implémentation du serveur MCP</h3>
<p>Pour le serveur MCP, nous allons réutiliser le serveur <a href="https://github.com/zed-industries/postgres-context-server/blob/main/index.mjs">Zed Postgres</a>, que nous allons légèrement modifier.</p>
<pre><code class="language-ts:mcp-server.ts">import { McpServer } from &quot;@modelcontextprotocol/sdk/server/mcp.js&quot;;
import { StdioServerTransport } from &quot;@modelcontextprotocol/sdk/server/stdio.js&quot;;
import {
  CallToolRequestSchema,
  ListToolsRequestSchema,
} from &quot;@modelcontextprotocol/sdk/types.js&quot;;
import pg from &quot;pg&quot;;

const { server } = new McpServer(
  {
    name: &quot;postgres&quot;,
    version: &quot;1.0.0&quot;,
  },
  {
    capabilities: {
      tools: {},
    },
  },
);

const stdioTransport = new StdioServerTransport();
</code></pre>
<p>On initialise ici un serveur MCP qui sera en capacité d&#x27;exposer uniquement les données liées aux tools. D&#x27;autre types de capabilities sont disponibles: <code>resources</code>, <code>prompts</code>, <code>completions</code>.
Nous utiliserons notre MCP sous forme d&#x27;exécutable. Par conséquent, la communication s&#x27;effectuera via la lecture et l&#x27;écriture sur <code>stdin</code> et <code>stdout</code>, d&#x27;où l&#x27;utilisation du transport <code>StdioServerTransport</code>. Si l&#x27;on souhaite utiliser le transport HTTP, nous devront utiliser <code>StreamableHTTPClientTransport</code>, et exposer notre serveur MCP à un port, en utilisant <code>express</code> par exemple.</p>
<pre><code class="language-ts:mcp-server.ts">const dbUrl = Bun.env.PG_URL;
const resourceBaseUrl = new URL(dbUrl!);
resourceBaseUrl.protocol = &quot;postgres:&quot;;
resourceBaseUrl.password = &quot;&quot;;

const pool = new pg.Pool({
  connectionString: Bun.env.PG_URL,
});
</code></pre>
<p>On initialise maintenant notre instance Postgres, afin d&#x27;établir une connexion plus tard.</p>
<p>Définissons maintenant l&#x27;accès à nos tools. Dans un premier temps nous devons en exposer la liste :</p>
<pre><code class="language-ts:mcp-server.ts">server.setRequestHandler(ListToolsRequestSchema, async () =&gt; {
  return {
    tools: [
      {
        name: &quot;pg-schema&quot;,
        description: &quot;Returns the schema for a Postgres database.&quot;,
        inputSchema: {
          type: &quot;object&quot;,
          properties: {
            mode: {
              type: &quot;string&quot;,
              enum: [&quot;all&quot;, &quot;specific&quot;],
              description: &quot;Mode of schema retrieval&quot;,
            },
            tableName: {
              type: &quot;string&quot;,
              description:
                &quot;Name of the specific table (required if mode is &#x27;specific&#x27;)&quot;,
            },
          },
          required: [&quot;mode&quot;],
          if: {
            properties: { mode: { const: &quot;specific&quot; } },
          },
          then: {
            required: [&quot;tableName&quot;],
          },
        },
      },
      {
        name: &quot;query&quot;,
        description: &quot;Run a read-only SQL query&quot;,
        inputSchema: {
          type: &quot;object&quot;,
          properties: {
            sql: { type: &quot;string&quot; },
          },
        },
      },
    ],
  };
});
</code></pre>
<p><code>ListToolsRequestSchema</code> est un schéma <a href="https://zod.dev/">Zod</a>. Si le serveur MCP reçoit une requête correspondant à ce schéma, alors il doit exécuter la fonction passée en second argument. Cette fonction doit retourner une liste de tools contenant un nom (servant d&#x27;identifiant théoriquement unique), une description qui sera lue par le modèle ainsi qu&#x27;un <a href="https://json-schema.org/">JSON Schema</a> permettant d&#x27;indiquer au modèle la structure de donnée attendue pour l&#x27;exécution du tool.</p>
<p>Maintenant pour l&#x27;exécution de nos tools :</p>
<pre><code class="language-ts:mcp-server.ts">server.setRequestHandler(CallToolRequestSchema, async (request) =&gt; {
  if (request.params.name === &quot;pg-schema&quot;) {
    const mode = request.params.arguments?.mode;

    const tableName = (() =&gt; {
      switch (mode) {
        case &quot;specific&quot;: {
          const tableName = request.params.arguments?.tableName;

          if (typeof tableName !== &quot;string&quot; || tableName.length === 0) {
            throw new Error(`Invalid tableName: ${tableName}`);
          }

          return tableName;
        }
        case &quot;all&quot;: {
          return ALL_TABLES;
        }
        default:
          throw new Error(`Invalid mode: ${mode}`);
      }
    })();

    const client = await pool.connect();

    try {
      const sql = await getSchema(client, tableName);

      return {
        content: [{ type: &quot;text&quot;, text: sql }],
      };
    } finally {
      client.release();
    }
  }

  if (request.params.name === &quot;query&quot;) {
    const sql = request.params.arguments?.sql as string;

    const client = await pool.connect();
    try {
      await client.query(&quot;BEGIN TRANSACTION READ ONLY&quot;);
      // Force a prepared statement: Prevents multiple statements in the same query.
      // Name is unique per session, but we use a single session per query.
      const result = await client.query({
        name: &quot;sandboxed-statement&quot;,
        text: sql,
        values: [],
      });
      return {
        content: [
          { type: &quot;text&quot;, text: JSON.stringify(result.rows, undefined, 2) },
        ],
      };
    } finally {
      client
        .query(&quot;ROLLBACK&quot;)
        .catch((error) =&gt;
          console.warn(&quot;Could not roll back transaction:&quot;, error),
        );

      // Destroy session to clean up resources.
      client.release(true);
    }
  }

  throw new Error(&quot;Tool not found&quot;);
});

/**
 * @param tableNameOrAll {string}
 */
async function getSchema(client, tableNameOrAll) {
  const select =
    &quot;SELECT column_name, data_type, is_nullable, column_default, table_name FROM information_schema.columns&quot;;

  let result;
  if (tableNameOrAll === ALL_TABLES) {
    result = await client.query(
      `${select} WHERE table_schema NOT IN (&#x27;pg_catalog&#x27;, &#x27;information_schema&#x27;)`,
    );
  } else {
    result = await client.query(`${select} WHERE table_name = $1`, [
      tableNameOrAll,
    ]);
  }

  const allTableNames = Array.from(
    new Set(result.rows.map((row) =&gt; row.table_name).sort()),
  );

  let sql = &quot;```sql\n&quot;;
  for (let i = 0, len = allTableNames.length; i &lt; len; i++) {
    const tableName = allTableNames[i];
    if (i &gt; 0) {
      sql += &quot;\n&quot;;
    }

    sql += [
      `create table &quot;${tableName}&quot; (`,
      result.rows
        .filter((row) =&gt; row.table_name === tableName)
        .map((row) =&gt; {
          const notNull = row.is_nullable === &quot;NO&quot; ? &quot;&quot; : &quot; not null&quot;;
          const defaultValue =
            row.column_default != null ? ` default ${row.column_default}` : &quot;&quot;;
          return `    &quot;${row.column_name}&quot; ${row.data_type}${notNull}${defaultValue}`;
        })
        .join(&quot;,\n&quot;),
      &quot;);&quot;,
    ].join(&quot;\n&quot;);
    sql += &quot;\n&quot;;
  }
  sql += &quot;```&quot;;

  return sql;
}
</code></pre>
<p>De la même manière que pour lister les tools, nous avons le schéma <code>CallToolRequestSchema</code> pour intercepter la requête de leur exécution.</p>
<p>On peut maintenant démarrer notre serveur :</p>
<pre><code class="language-ts:mcp-server.ts">const main = async () =&gt; {
  await server.connect(stdioTransport);

  // Dans le cas d&#x27;un transport HTTP
  // app.post(&quot;/mcp&quot;, async (req, res) =&gt; {
  //   await transport.handleRequest(req, res);
  // });

  // app.listen(Number(Bun.env.PORT ?? 3001));
};

main();
</code></pre>
<h3>Implémentation Next (Client)</h3>
<p>Créons maintenant notre page sur notre application Next.js, en utilisant le SDK AI de Vercel :</p>
<pre><code class="language-tsx:app/page.tsx">&quot;use client&quot;;
import { useChat } from &quot;ai/react&quot;;
import Markdown from &quot;react-markdown&quot;;

export default function HomePage() {
  const { messages, input, handleInputChange, handleSubmit } = useChat();

  return (
    &lt;div className=&quot;flex flex-col h-screen max-w-4xl mx-auto p-4&quot;&gt;
      &lt;div className=&quot;flex-1 overflow-y-auto mb-4&quot;&gt;
        {messages.map((message, index) =&gt; (
          &lt;div
            key={index}
            className={`mb-4 p-3 rounded-lg ${
              message.role === &quot;user&quot; ? &quot;bg-blue-100 ml-8&quot; : &quot;bg-gray-100 mr-8&quot;
            }`}
          &gt;
            &lt;div className=&quot;font-semibold text-sm text-gray-600 mb-1&quot;&gt;
              {message.role === &quot;user&quot; ? &quot;You&quot; : &quot;Assistant&quot;}
            &lt;/div&gt;
            &lt;div className=&quot;text-gray-800&quot;&gt;
              &lt;Markdown&gt;{message.content}&lt;/Markdown&gt;
            &lt;/div&gt;
          &lt;/div&gt;
        ))}
      &lt;/div&gt;

      &lt;form onSubmit={handleSubmit} className=&quot;flex gap-2&quot;&gt;
        &lt;input
          value={input}
          onChange={handleInputChange}
          placeholder=&quot;Type your message...&quot;
          className=&quot;flex-1 px-3 py-2 border border-gray-300 rounded-lg focus:outline-none focus:ring-2 focus:ring-blue-500&quot;
        /&gt;
        &lt;button
          type=&quot;submit&quot;
          className=&quot;px-4 py-2 bg-blue-500 text-white rounded-lg hover:bg-blue-600 focus:outline-none focus:ring-2 focus:ring-blue-500&quot;
        &gt;
          Send
        &lt;/button&gt;
      &lt;/form&gt;
    &lt;/div&gt;
  );
}
</code></pre>
<h3>Implémentation Next (Serveur)</h3>
<p>Créons maintenant notre route API <code>/api/chat</code>, utilisée par le hook <code>useChat</code>.</p>
<pre><code class="language-ts:app/api/chat/route.ts">import { anthropic } from &quot;@ai-sdk/anthropic&quot;;
import { experimental_createMCPClient, streamText } from &quot;ai&quot;;
import { Experimental_StdioMCPTransport } from &quot;ai/mcp-stdio&quot;;
import { NextRequest } from &quot;next/server&quot;;

export async function POST(req: NextRequest) {
  const { messages } = await req.json();
  const mcpTransport = new Experimental_StdioMCPTransport({
    command: &quot;bun&quot;,
    args: [&quot;pg:mcp&quot;],
  });

  const mcpClient = await experimental_createMCPClient({
    transport: mcpTransport,
    name: &quot;postgres-mcp-client&quot;,
  });

  try {
    const tools = await mcpClient.tools();

    const result = streamText({
      model: anthropic(&quot;claude-4-sonnet-20250514&quot;),
      messages,
      tools,
      maxSteps: 3,
      onError: async (error) =&gt; {
        await mcpClient.close();
      },
      onFinish: async () =&gt; {
        await mcpClient.close();
      },
    });

    return result.toDataStreamResponse();
  } catch (error) {
    console.error(&quot;Error:&quot;, error);
  }
}
</code></pre>
<p>Le SDK AI fournit des outils actuellement au stade expérimental permettant d&#x27;intéragir avec des serveurs MCP et d&#x27;en récupérer les tools.</p>
<pre><code class="language-ts">const mcpTransport = new Experimental_StdioMCPTransport({
  command: &#x27;bun&#x27;,
  args: [&#x27;pg:mcp&#x27;],
})

const mcpClient = await experimental_createMCPClient({
  transport: mcpTransport,
  name: &#x27;postgres-mcp-client&#x27;,
})
</code></pre>
<p>Ici on crée un transport lié à notre sortie standard utilisée par le binaire de notre serveur MCP. Ce binaire est exécuté via la commande <code>bun pg:mcp</code> que nous avons définie dans notre <code>package.json</code> :</p>
<pre><code class="language-json">&quot;pg:mcp&quot;: &quot;dotenv -e .env -- bun ./mcp-server.ts&quot;
</code></pre>
<p>L&#x27;appel à <code>mcpClient.tools()</code> renvoie une liste de tools compatible avec les API de tools du SDK AI, avec le bon nom, la bonne description, les bons paramètres, ainsi qu&#x27;une fonction d&#x27;exécution. Il est bien évidemment possible de se brancher à plusieurs serveurs MCP en même temps et d&#x27;en combiner les tools. Toutefois, il faut faire attention aux potentielles collisions qui peuvent survenir pour les noms de chacun. La propriété <code>maxSteps: 3</code> permet de continuer l&#x27;exécution de notre stream après l&#x27;appel à un tool. Par défaut, cette valeur est de 1. Ici on l&#x27;a définie à 3 pour permettre à nos tools de récupération du schéma et d&#x27;exécution de requêtes SQL d&#x27;être bien exécutés et renvoyés au client.</p>
<p>Testons maintenant l&#x27;ensemble ! Lançons notre serveur de développement Next :</p>
<pre><code class="language-sh">bun dev
</code></pre>
<p>Puis rendons-nous sur http://localhost:3000 pour voir notre application en action. Une page avec un simple champ de saisie en bas apparait. Dans mon cas, je choisis de faire la demande suivante :</p>
<blockquote>
<p>Donne moi la liste des 5 premiers email d&#x27;utilisateurs de ma base de données</p>
</blockquote>
<p><img src="https://www.premieroctet.com/blog/integration-mcp-dans-une-app-react/prompt.png" alt="liste des 5 premiers email d&#x27;utilisateurs de ma base de données"/></p>
<p>Le LLM a bien été capable de me sortir des données dont elle n&#x27;a, de base, pas connaissance, tout ça grâce à l&#x27;utilisations de tools récupérés depuis notre serveur MCP.</p>
<h2>Conclusion</h2>
<p>MCP a en quelques sortes révolutionné notre manière d&#x27;utiliser les LLMs. Leur capacité de s&#x27;interfacer entre les modèles et des API tierces rend l&#x27;accès aux données encore plus puissant qu&#x27;il ne l&#x27;était déjà. Ainsi on peut imaginer des cas d&#x27;utilisation sur des back-offices par exemple, simplifiant l&#x27;aggrégation de données provenant de sources multiples, le tout à partir d&#x27;une simple requête dans un language naturel. Les serveurs MCP peuvent aussi être installés sur des clients qui les supportent, par exemples les IDE mais aussi les applications de LLM (Claude, Chat GPT). Une liste très exhaustive de serveurs est disponible sur <a href="https://mcp.so/">mcp.so</a>, et vous trouverez surement le MCP qui répond à vos besoins.</p>
<p>Vous souhaitez implémenter un serveur MCP pour votre application ? N&#x27;hésitez pas à <a href="https://www.premieroctet.com/contact">nous contacter</a> !</p>]]></content:encoded>
            <enclosure url="https://www.premieroctet.com/blog/integration-mcp-dans-une-app-react/illu.png" length="0" type="image/png"/>
        </item>
        <item>
            <title><![CDATA[Doter votre site d'un agent IA sur mesure, c'est simple (et c'est souvent par là que tout commence)]]></title>
            <link>https://www.premieroctet.com/blog/doter-votre-site-d-un-agent-ia-sur-mesure-cest-simple-et-souvent-par-la-que-tout-commence</link>
            <guid>https://www.premieroctet.com/blog/doter-votre-site-d-un-agent-ia-sur-mesure-cest-simple-et-souvent-par-la-que-tout-commence</guid>
            <pubDate>Fri, 20 Jun 2025 00:00:00 GMT</pubDate>
            <description><![CDATA[Découvrez comment créer un agent IA personnalisé pour votre site web. Une solution simple et efficace pour intégrer l'intelligence artificielle dans vos projets.]]></description>
            <content:encoded><![CDATA[<p>Depuis quelques mois, on nous demande régulièrement si on est capable de développer un agent « à la ChatGPT » directement intégré à un site. La réponse est oui. Et mieux : c&#x27;est souvent le premier pas idéal pour introduire de l&#x27;IA dans votre projet.</p>
<p>Grâce à la popularité de ChatGPT, tout le monde connaît désormais cette interface en mode chat. Elle est simple à prendre en main, naturelle pour l&#x27;utilisateur, et c&#x27;est devenu un standard.</p>
<p>Chez Premier Octet, on s&#x27;appuie sur le <a href="https://ai-sdk.dev/">Vercel AI SDK</a> pour développer ce type d&#x27;interface. C&#x27;est l&#x27;état de l&#x27;art pour créer des expériences réactives, élégantes, avec un support natif du streaming des réponses et du tool calling (on y revient juste après).</p>
<h2>Qu&#x27;est-ce qu&#x27;un agent IA ?</h2>
<p>Un agent IA, c&#x27;est tout simplement une interface qui peut comprendre vos demandes en langage naturel et agir en conséquence. Contrairement à un chatbot classique avec des réponses préprogrammées, l&#x27;agent peut analyser le contexte, prendre des décisions et utiliser des outils pour accomplir des tâches concrètes.</p>
<p>Concrètement, l&#x27;agent peut tourner avec différents modèles de langage : GPT (OpenAI), Claude (Anthropic), Mistral, Gemini… On se connecte à leur API et on garde la liberté de personnaliser l&#x27;interface pour coller à l&#x27;identité de nos clients.</p>
<p>Pour rendre cet agent vraiment utile, il y a deux façons principales de le connecter à vos données métiers.</p>
<p>Et bonne nouvelle : elles sont complémentaires.</p>
<h2>Connecter l&#x27;agent à vos données métiers</h2>
<h3>La boite à outils</h3>
<p>La première approche consiste à donner des outils à l&#x27;agent. C&#x27;est ce qui va lui permettre d&#x27;agir : consulter une commande, chercher un bien immobilier, créer un ticket, ajouter un produit au panier…</p>
<p>Concrètement, comment ça marche ?</p>
<p>On commence par définir ensemble ce que l&#x27;agent doit savoir faire. Ensuite, on développe la &quot;boîte à outils&quot; sur mesure, que le modèle pourra appeler selon les besoins exprimés dans la conversation.</p>
<p>Chaque outil a un contrat clair : il attend certains paramètres (certains obligatoires, d&#x27;autres optionnels), et il renvoie une réponse structurée.</p>
<p>Prenons l&#x27;exemple d&#x27;un outil permetant de lancer <strong>une recherche d&#x27;annonces immobilières</strong>. Cet outil attend au minimum :</p>
<ul>
<li>un lieu</li>
<li>une surface minimale</li>
<li>un budget maximum</li>
</ul>
<p>Si l&#x27;utilisateur ne donne pas toutes les infos d&#x27;emblée, l&#x27;agent <strong>va naturellement poser les questions manquantes</strong>. Une fois les critères réunis, il déclenche l&#x27;outil, qui interroge votre API ou base de données. L&#x27;agent interprète la réponse et répond dans la foulée.</p>
<p>On a alors deux options :</p>
<ul>
<li>Soit on reste dans le chat pur : l&#x27;agent répond avec du texte, qu&#x27;on peut formater, styliser, cadrer.</li>
<li>Soit on va plus loin <strong>avec des widgets personnalisés dans le flux du chat</strong> : par exemple des cards reprenant l&#x27;UI du site, avec les annonces directement affichées.</li>
</ul>
<p><strong>Cette combinaison de langage naturel et de widgets métiers permet d&#x27;avoir une expérience utilisateur moderne, fluide, et surtout utile.</strong></p>
<p>Et évidemment, on peut aller très loin : prise de rendez-vous, ajout au panier, paiement, génération de documents… C&#x27;est une interface conversationnelle sur mesure.</p>
<h3>Transmettre de la connaissance métier</h3>
<p>La deuxième approche consiste à enrichir l&#x27;agent avec du savoir métier.</p>
<p>On parle ici de RAG (Retrieval-Augmented Generation). Pour résumé, on vectorise votre contenu (FAQ, fiches produits, documentation…) pour qu&#x27;il devienne compréhensible par le modèle de langage.</p>
<p>Une fois vectorisé, l&#x27;agent pourra répondre à des questions précises sur vos services, vos produits, vos conditions générales, etc., avec un niveau de détail et de pertinence qui dépasse les simples modèles &quot;out of the box&quot;.</p>
<p>Si vous souhaitez approfondir le sujet du RAG, je vous invite à lire <a href="https://www.premieroctet.com/blog/comment-fonctionne-un-rag">notre article sur le sujet</a>.</p>
<p>Ces deux approches sont bien sûr complémentaires. Elles permettent de couvrir des cas d&#x27;usage différents :</p>
<ul>
<li>un agent IA avec des outils pour interagir avec vos services</li>
<li>un agent IA avec du savoir métier pour répondre à des questions précises</li>
</ul>
<p>C&#x27;est souvent la combinaison de ces deux approches qui permet de couvrir la majorité des besoins.</p>
<p><img src="https://www.premieroctet.com/blog/doter-votre-site-d-un-agent-ia-sur-mesure-cest-simple-et-souvent-par-la-que-tout-commence/agent.png" alt="Agent IA"/></p>
<h2>Et le timing dans tout ça ?</h2>
<p>C&#x27;est souvent la question qui suit : combien de temps faut-il pour mettre ça en place ?</p>
<p><strong>5 à 10 jours suffisent pour une première version fonctionnelle, intégrée à votre site, avec une interface sur mesure et quelques outils bien choisis.</strong></p>
<p>On commence par cadrer ensemble : quelles capacités ? quels outils ? quelles données ? Une fois les specs définies, on développe, on teste, on connecte. Et on laisse l&#x27;IA faire sa part.</p>
<h2>Et le coût d&#x27;utilisation ?</h2>
<p>L&#x27;agent repose sur une API tierce (OpenAI, Anthropic, Mistral…). Ces APIs sont aujourd&#x27;hui accessibles et scalables. À titre d&#x27;exemple, voici les tarifs de l&#x27;API OpenAI GPT-4 Mini (en juin 2025) :</p>
<ul>
<li>0,40 $ / million de tokens en entrée</li>
<li>1,60 $ / million de tokens en sortie</li>
</ul>
<p>Pour une session de 10 échanges (Q/R), on parle généralement de moins d&#x27;un centime.</p>
<p>Il est aussi possible d&#x27;auto-héberger un modèle open source, mais cela nécessite une machine avec GPU. On peut aussi passer par des APIs sur étagères comme le propose OVH si l&#x27;on souhaite garder un contrôle total sur les données.</p>
<h2>En résumé</h2>
<p>Créer un agent IA pour votre site n&#x27;est plus un sujet &quot;futuriste&quot; ou &quot;expérimental&quot;. C&#x27;est concret, rapide à mettre en place, et surtout utile.</p>
<p>C&#x27;est aussi souvent une première brique pour aller plus loin ensuite : classification de contenu, génération de texte, aide à la rédaction, automatisation de tâches internes, génération de documents et assets…</p>
<p>Envie de tenter l&#x27;expérience ?</p>
<p><strong>Chez Premier Octet, on vous accompagne pour concevoir un agent IA aligné avec vos besoins, intégré à votre site, connecté à vos données.</strong></p>
<p><a href="https://www.premieroctet.com/contact">Contactez-nous</a> si vous avez envie d&#x27;en discuter ou si vous avez déjà une idée d&#x27;usage. On vous aide à la concrétiser en quelques jours.</p>
<p>👋</p>]]></content:encoded>
            <enclosure url="https://www.premieroctet.com/blog/doter-votre-site-d-un-agent-ia-sur-mesure-cest-simple-et-souvent-par-la-que-tout-commence/illu.jpg" length="0" type="image/jpg"/>
        </item>
        <item>
            <title><![CDATA[TanStack Start: la relève des frameworks React ?]]></title>
            <link>https://www.premieroctet.com/blog/tanstack-releve-frameworks-react</link>
            <guid>https://www.premieroctet.com/blog/tanstack-releve-frameworks-react</guid>
            <pubDate>Wed, 18 Jun 2025 00:00:00 GMT</pubDate>
            <description><![CDATA[Il y a quelques semaines de ça, une faille de sécurité a été découverte dans NextJS au niveau de la gestion de l'authentification au sein du fichier middleware...]]></description>
            <content:encoded><![CDATA[<p>Il y a quelques semaines de ça, une faille de sécurité a été découverte dans NextJS au niveau de la gestion de <a href="https://vercel.com/blog/postmortem-on-next-js-middleware-bypass">l&#x27;authentification au sein du fichier middleware</a>. Cette faille a été corrigée assez rapidement, mais Vercel n&#x27;a pas réellement communiqué dessus à vrai dire. Par la suite, les réseaux se sont enflammés et pour la plupart des développeurs, c&#x27;était la goutte de trop pour un framework qui souffrait déjà de bien des inconvénients notamment en terme de DX. J&#x27;ai pu voir passer de nombreux retours très positifs de la part de développeurs ayant complètement migré de NextJS vers <a href="https://tanstack.com/start">TanStack Start</a> (que nous nommerons <code>Start</code> pour la suite de cet article). Alors au final, est-ce que ce nouveau framework a des chances de concurrencer Remix ou NextJS, déjà bien en place dans l&#x27;écosystème React ?</p>
<h2>Présentation technique</h2>
<p>Start, créé par <a href="https://x.com/tannerlinsley">Tanner Linsley</a> que l&#x27;on ne présente plus, possède un routing basé sur une librairie déjà existante, <a href="https://tanstack.com/router">TanStack Router</a>. Utilisable de base pour les SPA, Start l&#x27;intègre notamment pour effectuer du SSR, à la manière de ce que fait <a href="https://remix.run/">Remix</a> avec <a href="https://reactrouter.com/">React Router</a>.</p>
<p>Côté bundling, Start se base sur <a href="https://vitejs.dev/">Vite</a>, nous permettant ainsi de bénéficier de tous les plugins Vite disponibles. Pour rappel, Next se base sur <a href="https://swc.rs/">SWC</a> ou <a href="https://turbo.build/pack">TurboPack</a> selon les versions.</p>
<p>Explorons plus en détails Start via une petite application toute simple avec une authentification basique, une liste de posts et les détails d&#x27;un post.</p>
<h2>Exploration</h2>
<h3>Mise en place</h3>
<p>Start propose un exemple de base à cloner faisant office de starter kit. Nous allons ici mettre en place un projet en partant de zéro, utilisant Tailwind CSS.</p>
<pre><code class="language-sh">mkdir starter-example
cd starter-example
yarn init -y
</code></pre>
<p>Installons maintenant les dépendances :</p>
<pre><code class="language-sh">yarn add @tanstack/react-start @tanstack/react-router react react-dom zod
yarn add -D vite @vitejs/plugin-react typescript @types/react @types/react-dom tailwindcss @tailwindcss/vite
</code></pre>
<p>Mettons en place nos différents fichiers de configuration :</p>
<pre><code class="language-json:tsconfig.json">{
  &quot;compilerOptions&quot;: {
    &quot;jsx&quot;: &quot;react-jsx&quot;,
    &quot;moduleResolution&quot;: &quot;Bundler&quot;,
    &quot;module&quot;: &quot;ESNext&quot;,
    &quot;target&quot;: &quot;ES2022&quot;,
    &quot;skipLibCheck&quot;: true,
    &quot;strictNullChecks&quot;: true,
  },
}
</code></pre>
<pre><code class="language-ts:vite.config.ts">import { defineConfig } from &#x27;vite&#x27;
import tailwindcss from &quot;@tailwindcss/vite&quot;;
import { tanstackStart } from &quot;@tanstack/react-start/plugin/vite&quot;;

export default defineConfig({
    plugins: [
      tanstackStart(),
      tailwindcss()
    ],
  },
})
</code></pre>
<p>Mettons à jour les scripts de notre <code>package.json</code> :</p>
<pre><code class="language-json:package.json">{
  &quot;scripts&quot;: {
    &quot;dev&quot;: &quot;vite dev&quot;,
    &quot;build&quot;: &quot;vite build&quot;,
    &quot;start&quot;: &quot;node .output/server/index.mjs&quot;
  }
}
</code></pre>
<p>Créons maintenant notre points d&#x27;entrée requis par Start. Celui-ci se situera dans le dossier <code>src</code> à la racine du projet.</p>
<pre><code class="language-tsx:src/router.tsx">import { createRouter as createTanStackRouter } from &#x27;@tanstack/react-router&#x27;
import { routeTree } from &#x27;./routeTree.gen&#x27;

export function createRouter() {
    const router = createTanStackRouter({
    routeTree,
    scrollRestoration: true,
    /*
        * il est ici possible de configurer les options par défaut du routeur,
        * comme par exemple le composant par défaut pour les routes en not found,
        */
    })

    return router

}

declare module &#x27;@tanstack/react-router&#x27; {
  interface Register {
    router: ReturnType&lt;typeof createRouter&gt;
  }
}
</code></pre>
<div type="info"><p>Le fichier <code>routeTree.gen</code> n&#x27;existe pas et c&#x27;est normal, il est généré automatiquement par le
bundler. C&#x27;est notamment lui qui est en charge de gérer le typage de nos routes.</p></div>
<p>Créons notre feuille de style CSS pour Tailwind :</p>
<pre><code class="language-css:app/styles/styles.css">@import &quot;tailwindcss&quot;;
</code></pre>
<p>Créons maintenant notre document HTML principal. C&#x27;est l&#x27;équivalent du fichier <code>layout.tsx</code> à la racine du dossier <code>app</code> d&#x27;une application Next. Ce fichier ce situe dans un sous-dossier <code>routes</code>. C&#x27;est dans ce sous-dossier que ce situeront toutes les routes de notre application.</p>
<pre><code class="language-ts:src/routes/__root.tsx">import type { ReactNode } from &quot;react&quot;;
import {
  Outlet,
  createRootRoute,
  HeadContent,
  Scripts,
} from &quot;@tanstack/react-router&quot;;
// import de la feuille de style au format URL
import appCss from &quot;../styles/styles.css?url&quot;;

export const Route = createRootRoute({
  head: () =&gt; ({
    meta: [
      {
        charSet: &quot;utf-8&quot;,
      },
      {
        name: &quot;viewport&quot;,
        content: &quot;width=device-width, initial-scale=1&quot;,
      },
      {
        title: &quot;Start Example&quot;,
      },
    ],
    links: [
      {
        rel: &quot;stylesheet&quot;,
        href: appCss,
      },
    ],
  }),
  component: RootComponent,
});

function RootComponent() {
  return (
    &lt;RootDocument&gt;
      &lt;Outlet /&gt;
    &lt;/RootDocument&gt;
  );
}

function RootDocument({ children }: Readonly&lt;{ children: ReactNode }&gt;) {
  return (
    &lt;html&gt;
      &lt;head&gt;
        &lt;HeadContent /&gt;
      &lt;/head&gt;
      &lt;body className=&quot;bg-white dark:bg-slate-900&quot;&gt;
        &lt;div className=&quot;min-h-screen flex flex-col&quot;&gt;{children}&lt;/div&gt;
        &lt;Scripts /&gt;
      &lt;/body&gt;
    &lt;/html&gt;
  );
}
</code></pre>
<p>Le composant <a href="https://tanstack.com/router/latest/docs/framework/react/guide/outlets"><code>Outlet</code></a> est un composant très utile pour construire des layouts imbriqués. Celui-ci rend le contenu de la route active et peut être utilisé à d&#x27;autres endroits de l&#x27;application. Nous verrons un cas d&#x27;utilisation plus tard.</p>
<p>Nous pouvons maintenant exécuter <code>yarn dev</code> pour démarrer notre bundler.</p>
<h3>Notre première route</h3>
<p>Créons un fichier <code>src/routes/index.tsx</code> pour notre première route. Si vous avez votre bundler d&#x27;ouvert, vous constaterez une fonctionnalité très intéressante venant de TanStack Router : un template de base pour le fichier avec tout le contenu nécessaire est généré. Celui-ci change en fonction du chemin du fichier. Par exemple, renommer le fichier changera le chemin de la route.</p>
<pre><code class="language-tsx:src/routes/index.tsx">import { createFileRoute, redirect } from &quot;@tanstack/react-router&quot;;

export const Route = createFileRoute(&quot;/&quot;)({
  component: Home,
});

function Home() {
  return (
    &lt;form
      className=&quot;flex flex-1 flex-col justify-center items-center gap-4&quot;
    &gt;
      &lt;div className=&quot;flex flex-col gap-2&quot;&gt;
        &lt;label htmlFor=&quot;username&quot; className=&quot;dark:text-white&quot;&gt;
          Username
        &lt;/label&gt;
        &lt;input
          name=&quot;username&quot;
          id=&quot;username&quot;
          placeholder=&quot;Username&quot;
          className=&quot;dark:text-white border dark:border-white&quot;
        /&gt;
        &lt;label htmlFor=&quot;password&quot; className=&quot;dark:text-white&quot;&gt;
          Password
        &lt;/label&gt;
        &lt;input
          name=&quot;password&quot;
          id=&quot;password&quot;
          placeholder=&quot;Password&quot;
          type=&quot;password&quot;
          className=&quot;dark:text-white border dark:border-white&quot;
        /&gt;
      &lt;/div&gt;
      &lt;button
        type=&quot;submit&quot;
        className=&quot;bg-blue-500 px-4 py-3 rounded-md text-white cursor-pointer&quot;
      &gt;
        Login
      &lt;/button&gt;
    &lt;/form&gt;
  );
}
</code></pre>
<p>En se rendant sur <code>http://localhost:3000</code>, notre page formulaire s&#x27;affiche correctement !</p>
<p>Mettons maintenant en place notre seconde route, qui sera notre liste de posts. Cette route devra être plus tard protégée par une authentification.</p>
<div><div><div value="src/routes/posts.tsx">src/routes/posts.tsx</div><div value="src/components/PostCard.tsx">src/components/PostCard.tsx</div></div><div value="src/routes/posts.tsx"><pre><code class="language-tsx">import { createFileRoute } from &quot;@tanstack/react-router&quot;;
import PostCard from &quot;../components/PostCard&quot;;
import { loadPosts } from &quot;../functions/posts&quot;;

export const Route = createFileRoute(&#x27;/posts&#x27;)({
  component: RouteComponent,
  loader: () =&gt; {
    return loadPosts()
  },
})

function RouteComponent() {
const loadedPosts = Route.useLoaderData();

return (

&lt;div className=&quot;flex flex-1 justify-center items-center&quot;&gt;
  &lt;div className=&quot;grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-6&quot;&gt;
    {loadedPosts.map((post) =&gt; (
      &lt;PostCard key={post.id} title={post.title} content={post.content} id={post.id} /&gt;
    ))}
  &lt;/div&gt;
&lt;/div&gt;
); }

</code></pre></div><div value="src/components/PostCard.tsx"><pre><code class="language-tsx">import { Link } from &quot;@tanstack/react-router&quot;;

type Props = {
  title: string;
  content: string;
  id: number;
};

const PostCard = ({ title, content, id }: Props) =&gt; {
  return (
    &lt;Link
      to=&quot;/posts/$id&quot;
      // Prefetch de la page au hover
      preload=&quot;intent&quot;
      params={{ id: id.toString() }}
      className=&quot;bg-white shadow-md rounded-lg p-6 mb-6 dark:shadow-white hover:scale-[101%] transition-transform duration-300 cursor-pointer&quot;
    &gt;
      &lt;h2 className=&quot;text-2xl font-bold mb-4&quot;&gt;{title}&lt;/h2&gt;
      &lt;p className=&quot;text-gray-700 line-clamp-3 text-ellipsis&quot;&gt;{content}&lt;/p&gt;
    &lt;/Link&gt;
  );
};

export default PostCard;
</code></pre></div></div>
<p>Ici on retrouve une API assez similaire à celle de Remix pour charger de la donnée côté serveur :</p>
<ul>
<li><code>loader</code> est exécuté au chargement de la page et renvoie notre donnée</li>
<li>on récupère cette donnée via le hook <code>useLoaderData</code> accessible depuis notre objet <code>Route</code>.</li>
</ul>
<p>Ici, notre liste de posts est chargée depuis une fonction serveur, l&#x27;opportunité de faire la connaissance avec les server functions de Start.</p>
<h3>Server functions</h3>
<p>Les server functions sont, comme celles de React et Next, des fonctions appelées depuis n&#x27;importe où (sauf les routes API) et exécutées côté serveur. Les server functions de Start ont toutefois des fonctionnalités en plus par rapport à ce que l&#x27;on retrouve par exemple sur Next :</p>
<ul>
<li>possibilité de définir la méthode HTTP à utiliser (POST ou GET)</li>
<li>ajout de validation (via <a href="https://zod.dev/">zod</a> par exemple)</li>
<li>accès à l&#x27;ensemble de la requête HTTP</li>
<li>possibilité de définir le type de réponse (JSON, stream)</li>
</ul>
<p>Créons notre fonction <code>loadPosts</code> :</p>
<div><div><div value="src/functions/posts.ts">src/functions/posts.ts</div><div value="src/mocks/posts.ts">src/mocks/posts.ts</div></div><div value="src/functions/posts.ts"><pre><code class="language-tsx">import { createServerFn } from &quot;@tanstack/react-start&quot;;
import { posts } from &quot;../mocks/posts&quot;;

export const loadPosts = createServerFn()
  .handler((async) =&gt; {
    return posts;
  });
</code></pre></div><div value="src/mocks/posts.ts"><pre><code class="language-tsx">export const posts = Array.from({ length: 10 }, (_, i) =&gt; ({
  id: i + 1,
  title: &quot;Lorem ipsum dolor sit amet.&quot;,
  content: &#x27;....lorem ipsum&#x27;,
}));
</code></pre></div></div>
<p>Profitons-en pour créer notre fonction de login pour notre formulaire ! Chose très intéressante, Start propose une solution clé en main pour gérer les sessions, que ce soit via le <a href="https://developer.mozilla.org/en-US/docs/Web/API/Window/sessionStorage">session storage</a> ou les cookies.</p>
<div><div><div value="src/functions/auth.ts">src/functions/auth.ts</div><div value="src/utils/session.ts">src/utils/session.ts</div><div value="src/routes/login.tsx">src/routes/login.tsx</div></div><div value="src/functions/auth.ts"><pre><code class="language-tsx">import { createServerFn, json } from &#x27;@tanstack/react-start&#x27;
import { setResponseStatus } from &#x27;@tanstack/react-start/server&#x27;
import { z } from &#x27;zod&#x27;
import { redirect } from &#x27;@tanstack/react-router&#x27;
import { setTimeout } from &#x27;node:timers/promises&#x27;
import { useAppSession } from &#x27;../utils/session&#x27;

export const loginAction = createServerFn({
  method: &#x27;POST&#x27;,
})
  .validator((data: unknown) =&gt; {
    const schema = z.object({
      username: z.string().min(1),
      password: z.string().min(1),
    })

    const result = schema.safeParse(data)

    if (!result.success) {
      throw json({ error: result.error.issues, message: &#x27;Validation failed&#x27; }, { status: 422 })
    }

    return result.data
  })
  .handler(async ({ data }) =&gt; {
    // Fake delay
    await setTimeout(1000)
    if (data.username !== &#x27;admin&#x27; &amp;&amp; data.password !== &#x27;admin&#x27;) {
      setResponseStatus(401)
      return {
        success: false,
        error: &#x27;Unauthorized&#x27;,
      }
    }

    const session = await useAppSession()

    await session.update({
      name: data.username,
    })

    // On redirige vers la page de posts après la connexion réussie
    throw redirect({
      to: &#x27;/posts&#x27;,
    })
  })

export const logoutAction = createServerFn({ method: &#x27;POST&#x27; }).handler(async () =&gt; {
  const session = await useAppSession()

  await session.clear()

  throw redirect({
    to: &#x27;/&#x27;,
  })
})
</code></pre></div><div value="src/utils/session.ts"><pre><code class="language-ts">import { json } from &#x27;@tanstack/react-start&#x27;
import { useSession } from &#x27;@tanstack/react-start/server&#x27;

type UserSession = {
  name: string
}

export const useAppSession = () =&gt; {
  return useSession&lt;UserSession&gt;({
    password: &#x27;MyPasswordAsALongStringOfCharactersOfAtLeast32Characters&#x27;,
    // On utilise les cookies, on peut configurer les options de cookie ici.
    // Par défaut, le session storage est utilisé.
    cookie: {},
  })
}
</code></pre></div><div value="src/routes/login.tsx"><pre><code class="language-tsx">import { createFileRoute } from &#x27;@tanstack/react-router&#x27;
import { useServerFn } from &#x27;@tanstack/react-start&#x27;
import { loginAction } from &#x27;../functions/auth&#x27;

export const Route = createFileRoute(&#x27;/&#x27;)({
  component: Home,
})

function Home() {
  const login = useServerFn(loginAction)

  return (
    &lt;form
      className=&quot;flex flex-1 flex-col justify-center items-center gap-4&quot;
      onSubmit={async (e) =&gt; {
        e.preventDefault()
        const formData = new FormData(e.target as HTMLFormElement)
        const username = formData.get(&#x27;username&#x27;) as string
        const password = formData.get(&#x27;password&#x27;) as string
        login({ data: { username, password } })
      }}
    &gt;
      &lt;div className=&quot;flex flex-col gap-2&quot;&gt;
        &lt;label htmlFor=&quot;username&quot; className=&quot;dark:text-white&quot;&gt;
          Username
        &lt;/label&gt;
        &lt;input
          name=&quot;username&quot;
          id=&quot;username&quot;
          placeholder=&quot;Username&quot;
          className=&quot;dark:text-white border dark:border-white&quot;
        /&gt;
        &lt;label htmlFor=&quot;password&quot; className=&quot;dark:text-white&quot;&gt;
          Password
        &lt;/label&gt;
        &lt;input
          name=&quot;password&quot;
          id=&quot;password&quot;
          placeholder=&quot;Password&quot;
          type=&quot;password&quot;
          className=&quot;dark:text-white border dark:border-white&quot;
        /&gt;
      &lt;/div&gt;
      &lt;button type=&quot;submit&quot; className=&quot;bg-blue-500 px-4 py-3 rounded-md text-white cursor-pointer&quot;&gt;
        Login
      &lt;/button&gt;
    &lt;/form&gt;
  )
}
</code></pre></div></div>
<p>Et voilà, après la connexion, vous êtes maintenant redirigé vers la liste des posts !</p>
<h3>Le concept de layout</h3>
<p>Un layout a pour but de créer une structure statique dans laquelle va s&#x27;intégrer le contenu de notre route. Avec Next, nous créons un layout via une route <code>layout.tsx</code> au même niveau que la route. Start (via l&#x27;intermédiaire du Router) a une approche très similaire à React Router pour gérer les layouts. Tant qu&#x27;une partie de notre URL est matchée, alors sa route est rendue.</p>
<p>Par exemple :</p>
<ul>
<li><code>/posts</code> -&gt; <code>&lt;RootDocument&gt;&lt;Posts&gt;</code></li>
<li><code>/posts/:id</code> -&gt; <code>&lt;RootDocument&gt;&lt;Posts&gt;&lt;Post&gt;</code></li>
</ul>
<p>Ce comportement est configurable en fonction du <a href="https://tanstack.com/router/latest/docs/framework/react/routing/file-naming-conventions">nom du fichier</a>. Par conséquent, par défaut, une route comme <code>/posts</code> que l&#x27;on a créé peut agir comme un layout pour <code>/posts/:id</code>. Le contenu de <code>/posts/:id</code> sera affiché via le composant <code>Outlet</code>.</p>
<p>Il est aussi possible de faire en sorte de créer un layout qui n&#x27;est pas lié à une route. C&#x27;est ce que l&#x27;on appelle un <code>pathless layout</code>. Celui-ci aura un nom de fichier préfixé d&#x27;un <code>_</code>.</p>
<p>Créons un layout utilisé au sein de nos routes authentifiées :</p>
<pre><code class="language-tsx:src/routes/_app.tsx">import { createFileRoute, Outlet, redirect } from &quot;@tanstack/react-router&quot;;
import { useServerFn } from &quot;@tanstack/react-start&quot;;
import { logoutAction } from &quot;../functions/auth&quot;;

export const Route = createFileRoute(&quot;/_app&quot;)({
  component: RouteComponent,
});

function RouteComponent() {
  const logout = useServerFn(logoutAction);

  return (
    &lt;div className=&quot;flex flex-col flex-1&quot;&gt;
      &lt;nav className=&quot;flex items-center justify-end py-3 px-4&quot;&gt;
        &lt;ul&gt;
          &lt;li
            className=&quot;dark:text-white hover:underline cursor-pointer&quot;
            onClick={() =&gt; {
              logout();
            }}
          &gt;
            Logout
          &lt;/li&gt;
        &lt;/ul&gt;
      &lt;/nav&gt;
      &lt;div className=&quot;px-4&quot;&gt;
        &lt;Outlet /&gt;
      &lt;/div&gt;
    &lt;/div&gt;
  );
}
</code></pre>
<p>Nous devons maintenant déplacer notre route <code>posts.tsx</code> dans un sous dossier <code>_app</code>, correspondant au nom de notre layout. Évidemment, nous pouvons donner le nom que l&#x27;on veut à notre layout. Il faut simplement garder une correspondance entre le nom du fichier du layout et le dossier créé.</p>
<p>Notre liste de posts est maintenant bien intégré au sein de notre layout. Il est évidemment possible de nester les layouts à l&#x27;infini, c&#x27;est le composant <code>Outlet</code> qui se charge de rendre les routes enfant à l&#x27;emplacement désiré.</p>
<p>Nous pouvons maintenant, grâce au layout, prévenir l&#x27;accès à nos pages authentifiées. En effet, un pathless layout dispose des mêmes possibilités d&#x27;usage d&#x27;une route. On peut donc utiliser la propriété <code>beforeLoad</code> afin de vérifier la connexion de l&#x27;utilisateur.</p>
<div><div><div value="src/routes/__root.tsx">src/routes/__root.tsx</div><div value="src/routes/_app.tsx">src/routes/_app.tsx</div></div><div value="src/routes/__root.tsx"><pre><code class="language-tsx">import type { ReactNode } from &quot;react&quot;;
import {
  Outlet,
  createRootRoute,
  HeadContent,
  Scripts,
} from &quot;@tanstack/react-router&quot;;
import { createServerFn } from &quot;@tanstack/react-start&quot;;
import appCss from &quot;../styles/styles.css?url&quot;;
import { useAppSession } from &quot;../utils/session&quot;;

const fetchUser = createServerFn({ method: &quot;GET&quot; }).handler(async () =&gt; {
const session = await useAppSession();

if (!session.data.name) {
return null;
}
return { name: session.data.name };
});

export const Route = createRootRoute({
  head: () =&gt; ({
    meta: [
      {
        charSet: &quot;utf-8&quot;,
      },
      {
        name: &quot;viewport&quot;,
        content: &quot;width=device-width, initial-scale=1&quot;,
      },
      {
        title: &quot;Start Example&quot;,
      },
    ],
    links: [
      {
        rel: &quot;stylesheet&quot;,
        href: appCss,
      },
    ],
  }),
  component: RootComponent,
  beforeLoad: async () =&gt; {
    const user = await fetchUser();

    return { user };

},
});

function RootComponent() {
return (

&lt;RootDocument&gt;
  &lt;Outlet /&gt;
&lt;/RootDocument&gt;
); }

function RootDocument({ children }: Readonly&lt;{ children: ReactNode }&gt;) {
return (

&lt;html&gt;
  &lt;head&gt;
    &lt;HeadContent /&gt;
  &lt;/head&gt;
  &lt;body className=&quot;bg-white dark:bg-slate-900&quot;&gt;
    &lt;div className=&quot;min-h-screen flex flex-col&quot;&gt;{children}&lt;/div&gt;
    &lt;Scripts /&gt;
  &lt;/body&gt;
&lt;/html&gt;
); }

</code></pre></div><div value="src/routes/_app.tsx"><pre><code class="language-tsx">import { createFileRoute, Outlet, redirect } from &quot;@tanstack/react-router&quot;;
import { useServerFn } from &quot;@tanstack/react-start&quot;;
import { logoutAction } from &quot;../functions/auth&quot;;

export const Route = createFileRoute(&quot;/_app&quot;)({
  component: RouteComponent,
  beforeLoad: async ({ context }) =&gt; {
    const isLoggedIn = !!context.user;

    if (!isLoggedIn) {
      throw redirect({
        to: &quot;/&quot;,
      });
    }
  },
});

function RouteComponent() {
  const logout = useServerFn(logoutAction);

  return (
    &lt;div className=&quot;flex flex-col flex-1&quot;&gt;
      &lt;nav className=&quot;flex items-center justify-end py-3 px-4&quot;&gt;
        &lt;ul&gt;
          &lt;li
            className=&quot;dark:text-white hover:underline cursor-pointer&quot;
            onClick={() =&gt; {
              logout();
            }}
          &gt;
            Logout
          &lt;/li&gt;
        &lt;/ul&gt;
      &lt;/nav&gt;
      &lt;div className=&quot;px-4&quot;&gt;
        &lt;Outlet /&gt;
      &lt;/div&gt;
    &lt;/div&gt;
  );
}
</code></pre></div></div>
<p>Et voilà, maintenant toutes les routes enfant de notre layout <code>_app</code> sont protégées !</p>
<h3>Routes API</h3>
<p>Comme tout framework full-stack qui se respecte, Start dispose d&#x27;un concept de routes API. La notation de fichier pour le routing est la même que pour nos pages classiques.</p>
<p>Créons une route API nous permettant de récupérer notre liste de posts :</p>
<pre><code class="language-ts:src/routes/api/posts/index.ts">import { json } from &quot;@tanstack/react-start&quot;;
import { createServerFileRoute } from &quot;@tanstack/react-start/server&quot;
import { posts } from &quot;../../../mocks/posts&quot;;

export const APIRoute = createServerFileRoute(&quot;/api/posts&quot;).methods({
  GET: async ({ request }) =&gt; {
    return json(posts);
  },
});
</code></pre>
<p>Que ce soit pour les server functions ou les routes API, il est possible d&#x27;utiliser tout un ensemble de fonctions utilitaires fournies par Stack afin de gérer à le contenu d&#x27;une requête et d&#x27;une réponse. C&#x27;est le cas de la fonction <code>json</code>. Par exemple si l&#x27;on souhaite renvoyer un statut HTTP particulier pour notre requête :</p>
<pre><code class="language-ts:src/routes/api/posts/index.ts">import { json } from &quot;@tanstack/react-start&quot;;
import { setResponseStatus, createServerFileRoute } from &quot;@tanstack/react-start/server&quot;;
import { posts } from &quot;../../../mocks/posts&quot;;

export const APIRoute = createServerFileRoute(&quot;/api/posts&quot;).methods({
  GET: async ({ request }) =&gt; {
    setResponseStatus(200);
    return json(posts);
  },
});
</code></pre>
<h3>Middlewares</h3>
<p>Les middlewares sont des fonctions que l&#x27;on branche aux server functions afin d&#x27;intercepter l&#x27;exécution de celles-ci. Elles permettent d&#x27;ajouter du contexte à nos requêtes, que ce soit lors de l&#x27;exécution côté client ou côté serveur.</p>
<p>Créons un middleware afin de rediriger l&#x27;utilisateur vers la page d&#x27;accueil s&#x27;il tente d&#x27;accéder à notre liste de posts en tant qu&#x27;utilisateur anonyme.</p>
<div><div><div value="src/middlewares/auth.ts">src/middlewares/auth.ts</div><div value="src/functions/posts.ts">src/functions/posts.ts</div></div><div value="src/middlewares/auth.ts"><pre><code class="language-ts">import { createMiddleware } from &quot;@tanstack/react-start&quot;;
import { redirect } from &quot;@tanstack/react-router&quot;;
import { useAppSession } from &quot;../utils/session&quot;;

export const authMiddleware = createMiddleware().server(async ({ next }) =&gt; {
  const user = await useAppSession();

if (!user.data.name) {
throw redirect({
to: &quot;/&quot;,
});
}

return next();
});

</code></pre></div><div value="src/functions/posts.ts"><pre><code class="language-ts">import { createServerFn } from &quot;@tanstack/react-start&quot;;
import { authMiddleware } from &quot;../middlewares/auth&quot;;
import { posts } from &quot;../mocks/posts&quot;;

export const loadPosts = createServerFn()
  .middleware([authMiddleware])
  .handler((async) =&gt; {
    return posts;
  });
</code></pre></div></div>
<p>Et voilà, maintenant le <code>handler</code> de notre server function ne s&#x27;exécutera uniquement si l&#x27;utilisateur est authentifié.</p>
<p>Un middleware est aussi utilisable sur une route API :</p>
<pre><code class="language-ts:src/routes/api/posts/index.ts">import { json } from &quot;@tanstack/react-start&quot;;
import { setResponseStatus, createServerFileRoute } from &quot;@tanstack/react-start/server&quot;;
import { posts } from &quot;../../../mocks/posts&quot;;
import { authMiddleware } from &quot;../../../middlewares/auth&quot;;

export const APIRoute = createServerFileRoute(&quot;/api/posts&quot;).methods((api) =&gt; ({
  GET: api.middleware([authMiddleware]).handler(async ({ request }) =&gt; {
    setResponseStatus(200);
    return json(posts);
  }),
}));
</code></pre>
<h3>DevTools</h3>
<p>Un gros quick win par rapport à Next : la possibilité de débugger les routes (les données du loader par exemple). Cela est disponible via les <a href="https://tanstack.com/router/latest/docs/framework/react/devtools">DevTools de TanStack Router</a>, à intégrer directement au sein du composant <code>RootDocument</code>.</p>
<p><img src="https://www.premieroctet.com/blog/tanstack-releve-frameworks-react/devtools.png" alt="DevTools TanStack Router" title="DevTools TanStack Router"/></p>
<h3>Déploiement</h3>
<p>Le déploiement d&#x27;une application Start est très simple. Différents presets sont fournis, à passer soit au sein de la configuration, soit via un flag au moment du build. Pour un simple serveur node par exemple :</p>
<pre><code class="language-sh">yarn build
node .output/server/index.mjs
</code></pre>
<p>Start supporte les services d&#x27;hébergements les plus importants du marché comme <a href="https://tanstack.com/start/latest/docs/framework/react/hosting#vercel">Vercel</a> et <a href="https://tanstack.com/start/latest/docs/framework/react/hosting#netlify">Netlify</a>.</p>
<h2>Conclusion</h2>
<p>Globalement j&#x27;ai eu une bonne expérience avec Start. Une DX très agréable, une documentation très complète (comme toutes les librairies TanStack) et toutes les fonctionnalités que l&#x27;on attends d&#x27;un framework full-stack. Le fonctionnement des server functions est, selon moi, bien mieux que sur NextJS (méthode <code>GET/POST</code>, réponse customisable). L&#x27;ajout de devtools liés au routing est aussi un énorme plus. C&#x27;est en tout cas une solution sur laquelle nous allons garder un oeil attentif, car c&#x27;est une alternative très intéressante par rapport à NextJS. Une idée de projet qui nécessiterait TanStack Start ? Utilisez <a href="https://www.premieroctet.com/estimateur-projet-web-mobile">notre estimateur de projet</a> !</p>]]></content:encoded>
            <enclosure url="https://www.premieroctet.com/blog/tanstack-releve-frameworks-react/illu.png" length="0" type="image/png"/>
        </item>
        <item>
            <title><![CDATA[Comment fonctionne un RAG ?]]></title>
            <link>https://www.premieroctet.com/blog/comment-fonctionne-un-rag</link>
            <guid>https://www.premieroctet.com/blog/comment-fonctionne-un-rag</guid>
            <pubDate>Tue, 13 May 2025 00:00:00 GMT</pubDate>
            <description><![CDATA[Découvrez comment le Retrieval-Augmented Generation (RAG) révolutionne les modèles de langage en leur permettant d’accéder à vos données privées sans hallucinations.]]></description>
            <content:encoded><![CDATA[<p>Ces dernières années ont été marquées par la démocratisation des <b>LLMs (Large Language Models)</b>, ces modèles de langage qui permettent de générer des textes sur la base de données générales. Aujourd&#x27;hui, l&#x27;intérêt qui leur est porté dépasse largement les frontières de la tech et des mathématiques. Les applications de ces modèles sont nombreuses et variées, allant de la génération de texte à la compréhension de la langue naturelle en passant par la traduction. En somme, des outils puissants qui ne cessent de se développer et de s&#x27;améliorer, mais qui souffrent encore de certaines limitations que nous allons aborder dans cet article.</p>
<h2>Introduction</h2>
<p>Avec la démocratisation de ces outils, beaucoup de personnes ont pu expérimenter et utiliser les modèles de langage &quot;génériques&quot;, je précise génériques car ces LLMs sont entraînés sur des données générales et ne sont donc pas spécialisés dans un domaine précis. Ce qui peut rapidement devenir un frein lorsque l&#x27;on souhaite générer des réponses précises et contextuelles. Les structures souhaitant exploiter les LLMs pour leurs besoins ont donc dû trouver des solutions pour améliorer la qualité des réponses générées. Deux solutions sont apparues :</p>
<ul>
<li><b>Fine-Tuning</b> sur un modèle générique</li>
<li>
<b>RAG (Retrieval-Augmented Generation)</b>
</li>
</ul>
<p>Dans cet article, nous allons nous aborder les inconvénients de la première solution, le Fine-Tuning, et nous concentrerons sur la seconde solution, le RAG, et comment il peut être utilisé pour améliorer la qualité des réponses générées par les LLMs et comment le mettre en place. Nous allons expliquer de manière détaillée comment fonctionne un RAG et comment le mettre en place de manière générale, il existe cependant de nombreuses variantes de ce système qui vont dépendre des modèles de langage et/ou des plateformes utilisées. L&#x27;idée est donc de comprendre les principes généraux et de pouvoir les appliquer à des cas concrets.</p>
<h2>TL;DR</h2>
<p>Pour résumer, un <code>RAG</code> combine la puissance des LLMs avec des connaissances spécifiques issues de vos documents. Ce système transforme d&#x27;abord vos données en représentations vectorielles (embeddings) stockées dans une base dédiée. Lorsqu&#x27;une question est posée, le système recherche automatiquement les informations les plus pertinentes dans cette base par <code>recherche vectorielle</code> ou <code>similarity search</code> et les ajoute comme contexte à la requête envoyée au LLM, permettant ainsi des réponses précises et basées sur vos données privées. Il permet également d&#x27;éviter les hallucinations qui peuvent survenir lors de l&#x27;utilisation de LLMs sur des sujets précis pour lesquels il n&#x27;a pas été entraîné.</p>
<p><img src="https://www.premieroctet.com/blog/comment-fonctionne-un-rag/rag.svg" alt="RAG"/></p>
<h2>Fine-Tuning</h2>
<p>Les LLMs sont des modèles de langage qui sont entraînés sur des données générales, ce qui leur permet de générer des réponses très performantes. Cependant, il est possible de les entraîner sur des données spécifiques à un domaine, ce qui leur permet de générer des réponses plus précises et contextuelles, on peut voir cela comme un sur-entraînement du modèle sur les données spécifiques. Cela peut être une solution efficace mais qui comporte des inconvénients :</p>
<ul>
<li>L&#x27;entraînement peut être coûteux en temps et en ressources</li>
<li>L&#x27;entraînement se fait sur des données statiques, il est donc nécessaire de mettre à jour le modèle régulièrement pour maintenir les connaissances à jour</li>
<li>Il est nécessaire de gérer les données d&#x27;entraînement, ce qui peut être une tâche complexe et fastidieuse</li>
</ul>
<p>Évidemment, cette solution n&#x27;est pas sans intérêt, elle permet dans certains usages de générer des réponses très performantes. Mais ici nous allons nous intéresser à un usage plutôt orienté <code>Question/Réponse</code> qui sera censé répondre de manière précise et contextuelle à une question. Pour cela, nous allons nous tourner et approfondir une autre solution: le <code>RAG</code>.</p>
<div type="info"><p>Pour des besoins spécifiques, il est possible de combiner les deux solutions. En utilisant dans un
RAG un modèle fine-tuned.</p></div>
<h2>Retrieval-Augmented Generation</h2>
<p>Un RAG pour <code>Retrieval-Augmented Generation</code> est un système qui va nous permettre d&#x27;enrichir nos questions envoyées à un LLM avec des informations contextuelles. Pour ce faire, il utilise l&#x27;<code>embedding</code> (ou <code>plongement vectoriel</code>) des documents et une base de données vectorielle pour stocker ces vecteurs. Cela nécessite de mettre en place et de remplir, en amont, la base de données vectorielle avec nos documents. Enfin, lors de l&#x27;envoi d&#x27;une question au LLM, celui-ci recevra non seulement la question originale mais également le contexte des documents les plus proches de la question grâce à la <code>recherche vectorielle</code> ou <code>similarity search</code>.</p>
<h2>Comment fonctionne un RAG ?</h2>
<p>Comme nous l&#x27;avons vu précédemment, une étape importante d&#x27;un RAG est de mettre en place et de remplir la base de données vectorielle avec nos données. Cette étape est primordiale pour le bon fonctionnement du RAG. Nous allons revenir en détails sur les différentes manières de créer ces embeddings.</p>
<p>Une fois que ces embeddings sont créés, lors de l&#x27;envoi d&#x27;une question au LLM, celle-ci sera vectorisée et comparée aux vecteurs des documents présents dans la base de données vectorielle. Cette comparaison permettra de récupérer les quelques morceaux de documents les plus pertinents et de les envoyer au LLM en plus de la question originale.</p>
<p>Il suffira alors au LLM de générer une réponse en s&#x27;appuyant sur le contexte fourni en entrée.</p>
<h2>Travail préliminaire</h2>
<h3>Les données d&#x27;entrée</h3>
<p>Une partie importante d&#x27;un RAG est la qualité des données utilisées pour créer les embeddings. En effet, plus les données sont pertinentes et contextuelles, plus la réponse générée par le LLM sera précise.</p>
<p>L&#x27;origine et le format des données d&#x27;entrée sont très variés. En effet, il peut s&#x27;agir de documents textuels, d&#x27;images, de pdf, de tableurs, et même de données extraites directement d&#x27;une base de données. Il est donc important de pouvoir les traiter de manière uniforme et de pouvoir les convertir en embeddings. <b>La seule contrainte est que les données doivent être convertibles en texte</b>.</p>
<p>Dans certains cas, il peut être nécessaire de convertir les données en texte. Par exemple, si les données sont des images, il est possible de les convertir en texte en utilisant des modèles de vision par ordinateur, ce qui permettra d&#x27;obtenir une description textuelle du contenu de l&#x27;image. Pour les PDFs, il est possible d&#x27;utiliser des modèles d&#x27;OCR (Optical Character Recognition) pour extraire le texte comme <a href="https://mistral.ai/news/mistral-ocr">Mistral OCR</a> par exemple.</p>
<h3>Partitionnement ou <code>chunking</code></h3>
<p>Le partitionnement ou &quot;chunking&quot; est l&#x27;étape qui consiste à découper nos documents en morceaux plus petits afin de les stocker dans notre base de données vectorielle. Cette étape est cruciale car elle influence directement la performance de notre RAG.</p>
<p><img src="https://www.premieroctet.com/blog/comment-fonctionne-un-rag/chunks.svg" alt="Partitionnement"/></p>
<h4>Taille des chunks</h4>
<p>La taille des chunks est un paramètre important à considérer lors de la mise en place d&#x27;un RAG. Il faut souvent expérimenter avec différentes tailles et différents niveaux de chevauchement pour trouver la configuration optimale. Les chunks plus petits permettent généralement une recherche plus précise, car ils contiennent moins de texte de remplissage qui pourrait diluer la représentation sémantique. Cela aide le système RAG à identifier et extraire plus efficacement les informations pertinentes. Toutefois, cette précision s&#x27;accompagne d&#x27;un coût : les chunks plus petits augmentent le temps de traitement et les ressources nécessaires.</p>
<h4>Méthodes de découpage</h4>
<p>Bien que la méthode la plus simple soit de découper le texte par caractère, d&#x27;autres options existent selon le cas d&#x27;utilisation et la structure du document :</p>
<ul>
<li>Par tokens : pour éviter de dépasser les limites de tokens dans les appels API</li>
<li>Par phrases ou paragraphes : pour maintenir la cohérence des chunks</li>
<li>Par en-têtes HTML ou propriétés JSON : pour respecter la structure du document</li>
<li>Par morceaux de code significatifs : si vous travaillez avec du code, il est souvent recommandé d&#x27;utiliser un analyseur d&#x27;arbre syntaxique abstrait (AST)</li>
</ul>
<h3>Embeddings</h3>
<p>Maintenant que nous avons nos données partitionnées, nous pouvons les convertir en embeddings, c&#x27;est-à-dire en vecteurs qui vont représenter nos données et nous permettre de capturer leur signification sémantique. Pour ce faire, nous allons utiliser un modèle d&#x27;<code>embedding</code> comme <a href="https://docs.mistral.ai/capabilities/embeddings/"><b>mistral-embed</b> de Mistral</a> ou <a href="https://platform.openai.com/docs/guides/embeddings"><b>text-embedding-3-small</b> de OpenAI</a> par exemple.</p>
<p><img src="https://www.premieroctet.com/blog/comment-fonctionne-un-rag/embed.svg" alt="Visualisation des embeddings"/></p>
<div type="info"><b>Pourquoi utiliser un modèle d&#x27;embedding plutôt que de générer des embeddings programmatiquement?</b><p>Il existe en effet de nombreuses manières programmatiques de générer des embeddings, mais l&#x27;utilisation d&#x27;un modèle d&#x27;embedding pré-entraîné permet de générer des embeddings de meilleure qualité car le modèle permet de mieux saisir le sens des données, l&#x27;intention et le contexte d&#x27;un chunk. Le vecteur généré sera donc plus pertinent.</p></div>
<h3>Base de données vectorielle</h3>
<p>La base de données vectorielle est un outil essentiel pour stocker et récupérer les embeddings. Elle va nous permettre de stocker les vecteurs et de les récupérer rapidement. Il existe plusieurs solutions pour mettre en place une base de données vectorielle, soit dédiées au RAG, soit intégrées à une pile logicielle existante si celle-ci le permet. Parmi les solutions les plus courantes, on peut citer <a href="https://www.pinecone.io/">Pinecone</a>, <a href="https://github.com/facebookresearch/faiss">Faiss</a>, <a href="https://github.com/spotify/annoy">Annoy</a>, <a href="https://github.com/nmslib/hnswlib">HNSW</a> ou encore <a href="https://github.com/milvus-io/milvus">Milvus</a>.</p>
<p>Si votre projet utilise déjà une base de données PostgreSQL, il est possible d&#x27;utiliser l&#x27;extension <a href="https://github.com/pgvector/pgvector">pgvector</a> pour stocker et récupérer les embeddings. Cela permet de bénéficier d&#x27;une base de données vectorielle nativement intégrée à votre projet et donc de créer des liens entre vos données et vos embeddings. Très utile lorsque l&#x27;on souhaite récupérer des informations relatives à des documents.</p>
<p><img src="https://www.premieroctet.com/blog/comment-fonctionne-un-rag/vector-db.svg" alt="Base de données vectorielle"/></p>
<p>Voilà, nous avons maintenant nos données partitionnées, nos embeddings et notre base de données vectorielle. Nous pouvons maintenant passer à la phase d&#x27;interprétation des questions et de génération de réponses en créant un Assistant RAG.</p>
<h2>Création d&#x27;un Assistant RAG</h2>
<p>Cette manière d&#x27;utiliser un RAG va fortement dépendre de la plateforme utilisée et/ou du framework utilisé, dans certains cas, il sera possible de créer un Assistant RAG découplé de votre application, comme sur la plateforme OpenAI. Autrement, il sera nécessaire de créer un Assistant RAG couplé à votre application en définissant les outils et la manière de les utiliser explicitement directement dans le code de votre application.</p>
<h3>Définir les outils</h3>
<p>Par outil, nous entendons les différentes fonctions qui vont être utilisées pour récupérer les données dans la base de données vectorielle et les utiliser pour générer des réponses précises et contextuelles.</p>
<h4>Recherche vectorielle</h4>
<p>La recherche vectorielle est la fonction qui va permettre de récupérer les données dans la base de données vectorielle en fonction de la question.</p>
<p>Dans un premier temps, il faudra <b>transformer la question en vecteur</b> grâce au modèle d&#x27;embedding que nous avons utilisé pour créer nos embeddings plus tôt. De cette manière, nous pourrons comparer le vecteur de la question à tous les vecteurs des documents présents dans la base de données vectorielle à l&#x27;aide d&#x27;une fonction de recherche vectorielle comme <code>similarity</code> avec l&#x27;extension <code>pgvector</code> de PostgreSQL.</p>
<p><img src="https://www.premieroctet.com/blog/comment-fonctionne-un-rag/vector-search.svg" alt="Recherche vectorielle"/></p>
<p>Cette recherche vectorielle va nous permettre d&#x27;obtenir les documents les plus proches de la question, c&#x27;est-à-dire les documents qui ont le plus de ressemblance avec la question.</p>
<p>Pour l&#x27;exemple, nous allons utiliser la fonction <code>similarity</code> avec l&#x27;extension <code>pgvector</code> de PostgreSQL.</p>
<pre><code class="language-ts:tool.ts">export const findRelevantContent = async (userQuery: string) =&gt; {
  const embedding = await generateEmbedding(userQuery);
  const vectorQuery = `[${embedding.join(&quot;,&quot;)}]`;
  const embeddings = await db.$queryRaw
  `
      SELECT
        embeddings.id,
        embeddings.content,
        documents.name,
        1 - (embedding &lt;=&gt; ${vectorQuery}::vector) as similarity
      FROM embeddings
      INNER JOIN documents ON embeddings.document_id = documents.id
      WHERE 1 - (embedding &lt;=&gt; ${vectorQuery}::vector) &gt; .5
      ORDER BY similarity DESC
      LIMIT 5;
    `;
  return embeddings;
};
</code></pre>
<p>Ici on limite la recherche à 5 chunks ayant une similarité supérieure à 0.5. Cette valeur est arbitraire et peut être ajustée en fonction de la pertinence des résultats.</p>
<h3>Prompt</h3>
<p>Le prompt est le texte qui va être envoyé au LLM pour générer une réponse. Il va contenir les directives pour le LLM afin qu&#x27;il génère une réponse pertinente en lui précisant d&#x27;utiliser l&#x27;outil de recherche vectorielle pour récupérer les documents les plus proches de la question.</p>
<p>C&#x27;est dans cette partie qu&#x27;il faudra également préciser à l&#x27;assistant comment il doit utiliser le contexte des documents.</p>
<p>Pour l&#x27;exemple, nous allons utiliser le SDK de Vercel pour générer une réponse en lui précisant d&#x27;utiliser l&#x27;outil de recherche vectorielle pour récupérer les documents les plus proches de la question.</p>
<pre><code class="language-ts:generate-answer.ts">export async function POST(req: Request) {
  const { messages } = await req.json();
  const result = await streamText({
    model: model,
    messages: convertToCoreMessages(messages),
    system: `You are a helpful assistant. Check your knowledge base before answering any questions.
    Only respond to questions using information from tool calls.
    If no relevant information is found in the tool calls, respond, &quot;Sorry, I don&#x27;t know.&quot;`,
    tools: {
      getInformation: tool({
        description: `get information from your knowledge base to answer questions.`,
        parameters: z.object({
          question: z.string().describe(&quot;the users question&quot;),
        }),
        execute: async ({ question }) =&gt; findRelevantContent(question),
      }),
    },
    toolChoice: &quot;required&quot;,
  });
  return result.toAIStreamResponse();
}
</code></pre>
<div type="info"><p>La manière d&#x27;imposer à l&#x27;assistant l&#x27;utilisation de l&#x27;outil de recherche vectorielle dépendra du SDK
utilisé.</p></div>
<h2>Conclusion</h2>
<p>Vous l&#x27;aurez compris, le <code>RAG</code> est un outil puissant qui peut être utilisé pour générer des réponses précises et contextuelles et éviter tout problème d&#x27;hallucinations, fréquent lors de l&#x27;utilisation de LLMs sur des sujets complexes. En orientant et en contextualisant les questions envoyées au LLM, il est possible d&#x27;obtenir des réponses qui s&#x27;appuient réellement sur vos données.</p>
<p>Les cas d&#x27;utilisation sont nombreux, chez Premier Octet, nous avons déjà pu mettre en place des RAGs sur des projets de chatbots et de génération de réponses, pour des structures qui souhaitaient utiliser la puissance des LLMs tout en l&#x27;adaptant à leurs propres besoins et leurs données.</p>
<p>Si vous avez des retours d&#x27;expérience sur ces sujets-là, ou que vous souhaitez en savoir plus sur le RAG, n&#x27;hésitez pas à nous contacter.</p>
<h2>Références</h2>
<ul>
<li><a href="https://mistral.ai/news/mistral-ocr">Mistral OCR</a> - Optical Character Recognition Model</li>
<li><a href="https://platform.openai.com/docs/guides/embeddings">OpenAI</a> - Embedding Model</li>
<li><a href="https://www.pinecone.io/">Pinecone</a> - Vector Database</li>
<li><a href="https://github.com/pgvector/pgvector">pgvector</a> - Vector Extension for PostgreSQL</li>
<li><a href="https://ai-sdk.dev/docs/introduction">Vercel AI SDK</a> - AI SDK</li>
<li><a href="https://js.langchain.com/docs/introduction/">LangChain</a> - AI SDK</li>
</ul>]]></content:encoded>
            <enclosure url="https://www.premieroctet.com/blog/comment-fonctionne-un-rag/illu.webp" length="0" type="image/webp"/>
        </item>
        <item>
            <title><![CDATA[Mon premier projet en React : Picksale]]></title>
            <link>https://www.premieroctet.com/blog/picksale-mon-premier-projet-en-react</link>
            <guid>https://www.premieroctet.com/blog/picksale-mon-premier-projet-en-react</guid>
            <pubDate>Tue, 22 Apr 2025 00:00:00 GMT</pubDate>
            <description><![CDATA[Dans cet article, je vais vous parler de Picksale, le tout premier projet de ma carrière...]]></description>
            <content:encoded><![CDATA[<p>Face à cette page blanche, difficile de ne pas repenser à mes débuts en tant qu&#x27;apprentie développeuse, ce moment où je scrutais mon écran, en quête du point de départ. Aujourd’hui, j’aimerais vous parler de <strong>Picksale</strong>, mon tout premier projet, celui grâce auquel j’ai appris l’essentiel de ce que je sais en développement.</p>
<p>J’espère que ce récit motivera celles et ceux qui commencent tout juste leur parcours. Et pour les développeurs plus expérimentés, peut-être vous rappellera-t-il avec une pointe de nostalgie vos premières années de junior.</p>
<h2>Picksale : une toile vierge</h2>
<p><strong>Une plateforme e-commerce qui permet aux créateurs d’ouvrir leur boutique en ligne.</strong> Voilà les grandes lignes du projet que l’on m’avait confié il y a 2 ans. Je me rappelle avoir hoché la tête avec l’assurance d’une experte, mais une fois face à ma toile virtuelle, j’avais vite pris conscience de l’ampleur de la tâche.</p>
<p><img src="https://www.premieroctet.com/blog/picksale-mon-premier-projet-en-react/easel-empty.png" alt="Img"/></p>
<p>Si un artiste peut être paralysé par une toile blanche, un développeur junior l’est tout autant devant un repo fraîchement cloné. Heureusement, <strong>je n’étais pas seule dans cet atelier numérique</strong>. Mon tuteur et mes collègues étaient là pour m’aider à mélanger mes premières couleurs. Ils n’ont pas seulement répondu à mes questions, <strong>ils m’ont également appris à chercher les réponses par moi-même.</strong></p>
<p>Et dans cette quête, voici la palette d’outils qui m’avait été confiée pour donner vie à Picksale :</p>
<p>🎨 <strong><a href="https://fr.react.dev/">React</a> &amp; <a href="https://nextjs.org/">Next.js</a></strong> – Mon pinceau et ma palette. Next.js m’a enseigné l’art du rendu client et serveur, des concepts qui, à l’époque, me semblaient aussi mystérieux qu’un tableau abstrait.</p>
<p>🖍 <strong><a href="https://chakra-ui.com/">Chakra UI</a></strong> – Un set de couleurs prêt à l’emploi. Plutôt que de réinventer la roue en CSS, j’ai appris à styliser les composants avec une simplicité déconcertante. Un peu comme peindre avec des numéros : utilisez les bons composants, et tout prend forme.</p>
<p>🛠 <strong><a href="https://www.prisma.io/">Prisma</a></strong> – Mon guide pour dompter la base de données. Manipuler PostgreSQL directement ? Trop brutal pour mes débuts. Prisma a été mon pinceau fin, me permettant d’écrire et de lire les données avec simplicité et précision.</p>
<p>💳 <strong><a href="https://stripe.com/fr">Stripe</a></strong> – Le vernis final. Gérer des paiements en ligne ? Un défi de taille. Mais avec Stripe, j’ai compris que la complexité pouvait être encapsulée derrière des API bien pensées. J’ai encore ce frisson du premier paiement réussi sur Picksale, un peu comme voir son tableau encadré dans une galerie.</p>
<h2>Une œuvre d’art… vraiment ?</h2>
<p><img src="https://www.premieroctet.com/blog/picksale-mon-premier-projet-en-react/goofy-unicorn.png" alt="Img"/></p>
<p><strong>S’engager sans planifier, c’est planifier son échec</strong>. Moi, j’ai non seulement planifié, mais j’ai aussi consacré des heures à concevoir… une véritable monstruosité. Quand je repense à ma première maquette, je ne peux m’empêcher de rire (et de rougir). Dans mon esprit, le rendu était élégant, innovateur, minimaliste… dans la réalité, c’était un cauchemar pastel sur fond de naïveté.</p>
<p><strong>Si je devais vous décrire cette œuvre d’art, ce serait une agression visuelle</strong> : une palette de couleurs à faire pleurer un arc-en-ciel, des boutons aussi massifs qu’un donut, et un design qui ferait pâlir un site web des années 90. <strong>Heureusement, l’un de mes collègues a fini par intervenir et a sauvé la direction artistique de Picksale en lui offrant un coup de pinceau moderne et épuré.</strong> Malgré cette refonte salvatrice, mon chef-d’œuvre original reste figé dans les commits de GitHub et les déploiements de Vercel.</p>
<p>J’espère juste que personne ne sera assez curieux pour la déterrer… ou qu’il fera preuve d’un peu d’indulgence dans ses critiques.</p>
<h2>Apprendre à peindre à plusieurs</h2>
<p><img src="https://www.premieroctet.com/blog/picksale-mon-premier-projet-en-react/two-brushes.png" alt="Img"/></p>
<p>Après des mois de travail acharné, Picksale était devenu mon bébé. <strong>Mon code, mes décisions, mon précieux.</strong> Si quelqu’un proposait une modification, j&#x27;avais tendance à me montrer aussi protectrice qu’un chat qui garde sa pâtée.</p>
<p><strong>Avec le temps, heureusement, j&#x27;ai appris à lâcher prise et à ouvrir mon esprit</strong>. Travailler avec ma collègue m&#x27;a aidée à comprendre que le travail d&#x27;équipe n&#x27;était pas une contrainte, mais un atout. Au début, ses choix de couleurs et de formes étaient différents de ce que j&#x27;aurais imaginé. Mais petit à petit, <strong>j&#x27;ai compris que j’avais également accès à sa palette unique : son expertise, sa vision, et sa manière de voir les choses autrement.</strong></p>
<p>La collaboration, ce n&#x27;est pas juste un compromis, mais plutôt une fusion d&#x27;idées qui donne quelque chose de bien plus fort et riche que ce que j&#x27;aurais pu créer seule. <strong>Un projet, c’est comme un grand tableau où chaque touche compte, même celles qui ne viennent pas de notre pinceau.</strong></p>
<h2>L’Art du Débugging</h2>
<p><img src="https://www.premieroctet.com/blog/picksale-mon-premier-projet-en-react/spilled-paint.png" alt="Img"/></p>
<p>Si au début tout ressemblait à une belle toile blanche, une fois Picksale arrivé à maturité, le tableau s’est compliqué. <strong>Bugs imprévisibles, pertes de données mystérieuses, comportements étranges surgissant uniquement en production</strong> (bien évidemment).</p>
<p>Chaque jour amenait son lot de surprises, rarement agréables. J’ai connu le stress de voir une fonctionnalité casser sans raison apparente et l’angoisse de corriger un bug qui en générait trois nouveaux. Face à ma détresse, un collègue (apôtre des tests automatisés) m’a tendu la main : <strong>l&#x27;heure était venue de faire entrer <a href="https://playwright.dev/">Playwright</a> dans nos vies</strong>. Depuis, à chaque pull request, cet outil se dresse tel un critique d&#x27;art intransigeant — parfois bienveillant, souvent cruel, mais toujours juste.</p>
<p>Aujourd&#x27;hui, à force de plonger dans ces problèmes, <strong>j’ai appris à garder mon sang-froid</strong> et à affiner ma méthode. Même si l’idée de tomber sur un bug compliqué me donne toujours des sueurs froides, <strong>je sais que j’ai les outils et l’expérience nécessaires pour le régler</strong>. Après tout, le développement, c’est un peu comme la restauration d’un tableau ancien : il y aura toujours des fissures à réparer, mais <strong>avec de la patience et les bons outils, rien n’est irrécupérable.</strong></p>
<h2>Et aujourd’hui ?</h2>
<p>Ce projet qui m’a tant appris est désormais <strong>une plateforme robuste</strong> qui a pour but de pour simplifier la vie des artistes, designers et petits artisans du web.</p>
<p><strong>Avec Picksale, chacun peut modeler sa boutique à son image</strong> :</p>
<p>🎨 <strong>Personnalisation avancée</strong> – Choisissez vos couleurs, votre typographie et ajoutez des bannières pour créer un espace qui vous ressemble.</p>
<p>📄 <strong>Pages CMS et Portfolio</strong> – Racontez votre histoire, partagez votre univers et exposez vos créations comme dans une vraie galerie.</p>
<p>🚚 <strong>Système de livraison flexible</strong> – Après un long travail de réflexion et plusieurs refonte, Picksale propose désormais un système de profils de livraison pensé dans les moindres détails, conçu pour gérer les ventes locales et internationales.</p>
<p>📦 <strong>Gestionnaire de commandes</strong> – De la confirmation à l’envoi, suivez vos ventes et tenez vos clients informés grâce aux e-mails automatique.</p>
<p>🛠 <strong>Et bien plus encore</strong> – Picksale continue d’évoluer avec toujours plus d’outils pour accompagner ses utilisateurs !</p>
<p><img src="https://www.premieroctet.com/blog/picksale-mon-premier-projet-en-react/picksale-preview.gif" alt="Img"/></p>
<p>Aujourd’hui, <strong>la plateforme compte plus de 100 boutiques et près de 600 commandes passées.</strong> Pour ceux qui veulent aller encore plus loin, le plan Pro propose un tableau de bord analytique alimenté par Plausible pour suivre ses performances, des codes de réduction, des e-mails personnalisés et <strong>une tonne de nouveautés à venir !</strong></p>
<p>Si vous cherchez un moyen rapide et efficace de lancer votre boutique en ligne, je ne peux que vous inviter à <a href="https://picksale.app/">y jeter un coup d&#x27;œil</a>. <strong>Et qui sait ? Peut-être que votre propre aventure débutera sur cette même toile blanche qui a tant compté pour moi.</strong></p>
<h2>Conclusion</h2>
<p><img src="https://www.premieroctet.com/blog/picksale-mon-premier-projet-en-react/easel-full.png" alt="Img"/></p>
<p>Comme vous l’aurez compris, tout ne s’est pas fait en un coup de pinceau. J’ai écrit tellement de lignes de code avec fierté... avant de les effacer dans la panique quelques jours plus tard. Entre mes useEffect qui tournaient en boucle et mes composants qui se rafraîchissaient de manière épileptique, <strong>j’avais parfois l’impression que mon application prenait vie… dans le mauvais sens du terme.</strong></p>
<p><strong>Mais chaque bug était une retouche, chaque refactorisation un coup de pinceau plus précis</strong>. Peu à peu, j’ai appris à voir le code autrement : non plus comme une simple liste d’instructions, mais comme <strong>une œuvre évolutive qui se perfectionne ligne après ligne</strong>.</p>
<p>Deux ans plus tard, <strong>Picksale est devenu un projet solide avec une équipe agrandie, et moi, je suis devenue une développeuse bien plus confiante</strong>. Je ne saurais vous décrire la joie de recevoir les premiers retours positifs de nos utilisateurs, ce compteur grimpe petit à petit avec le temps et c’est une grande source de fierté que de voir des personnes utiliser un outil que l’on a imaginé.</p>
<p>Si vous êtes junior et que vous doutez, sachez ceci : <strong>au début, on a tous l’impression de peindre avec des moufles</strong>. Mais avec du temps, des erreurs et des bons conseils, <strong>on finit par maîtriser ses coups de pinceau</strong>.</p>
<p>Alors accrochez-vous, demandez de l’aide, et surtout, continuez à peindre. Un jour, vous regarderez votre premier projet avec la même fierté que j’ai aujourd’hui en pensant à Picksale.</p>
<p><strong>Bon code et belles toiles !</strong> 🎨💻</p>]]></content:encoded>
            <enclosure url="https://www.premieroctet.com/blog/picksale-mon-premier-projet-en-react/picksale.png" length="0" type="image/png"/>
        </item>
        <item>
            <title><![CDATA[Lynx: le remplaçant de React Native ?]]></title>
            <link>https://www.premieroctet.com/blog/lynx-remplacant-de-react-native</link>
            <guid>https://www.premieroctet.com/blog/lynx-remplacant-de-react-native</guid>
            <pubDate>Tue, 15 Apr 2025 00:00:00 GMT</pubDate>
            <description><![CDATA[Sorti en open source il y a maintenant plusieurs semaines, Lynx est un nouvel arrivant dans le monde du développement mobile.]]></description>
            <content:encoded><![CDATA[<p>Sorti en open source il y a maintenant plusieurs semaines, <a href="https://lynxjs.org/">Lynx</a> est un nouvel arrivant dans le monde du développement mobile. Développé à l&#x27;origine par ByteDance, la librairie est déjà utilisée en production au sein de l&#x27;application TikTok, de la même manière que Facebook utilisait <a href="https://reactnative.dev/">React Native</a> au sein de ses applications mobiles. Maintenant disponible au grand public, la question se pose de savoir si Lynx peut devenir un concurrent voire un remplaçant de React Native.</p>
<h2>Comparatif technique</h2>
<p>En parcourant la documentation officielle, on se rend tout de suite compte que Lynx a puisé beaucoup d&#x27;inspiration sur des frameworks déjà existants, notamment React Native et Flutter, afin d&#x27;offrir une expérience de développement très proche de celle que l&#x27;on pourrait retrouver sur une application Web :</p>
<ul>
<li>Bundler moderne basé sur <a href="https://rspack.dev/">Rspack</a></li>
<li><a href="https://lynxjs.org/react/">Librairie React</a> basée sur React 17, permettant de faire le pont avec les composants natifs</li>
<li>Support des feuilles de style CSS et de la majorité des propriétés CSS</li>
<li>Support des sélecteurs, que ce soit pour du CSS ou <a href="https://lynxjs.org/api/lynx-api/main-thread/main-thread-element.html">au runtime</a></li>
</ul>
<p>De la même manière qu&#x27;<a href="https://expo.dev/go">Expo Go</a>, Lynx fournit une application à installer sur son simulateur, <a href="https://lynxjs.org/guide/start/quick-start.html#ios-simulator-platform=macos-arm64,explorer-platform=ios-simulator">Lynx Explorer</a>, permettant d&#x27;exécuter un bundle depuis une URL (locale ou distante). C&#x27;est d&#x27;ailleurs grâce à cela qu&#x27;il est possible d&#x27;exécuter les différents exemples fournis dans la documentation.</p>
<p>Une application de <a href="https://lynxjs.org/guide/debugging/lynx-devtool.html">DevTools</a> est aussi disponible avec un inspecteur d&#x27;élément (HTML et natifs) et une console JavaScript. On notera l&#x27;absence d&#x27;un inspecteur de réseau, fortement appréciable du côté des DevTools Expo.</p>
<p>Côté runtime, on retrouve <a href="https://github.com/lynx-family/primjs">PrimJS</a> en tant que moteur JavaScript, développé spécifiquement pour Lynx, de la même manière que React Native utilise <a href="https://hermesengine.dev/">Hermes</a>.</p>
<p>Niveau support, Lynx a été pensé pour supporter les plateformes mobiles ainsi que le web, de la même manière que React Native supporte Android, iOS (Windows, MacOS, Web supportés aussi par l&#x27;intermédiaire de librairies tierces).</p>
<h2>La découverte par la pratique</h2>
<p>Pour découvrir plus en détail Lynx, nous allons développer une simple application de deux écrans, utilisant l&#x27;API <a href="https://jsonplaceholder.typicode.com/">JSON Placeholder</a>, pour lister des posts, accéder aux détails d&#x27;un post et y lister des commentaires.</p>
<h3>Création du projet</h3>
<p>Créer un projet Lynx est assez simple :</p>
<pre><code class="language-bash">yarn create rspeedy
</code></pre>
<p>Après que le projet soit créé, il ne reste plus qu&#x27;à installer les dépendances :</p>
<pre><code class="language-bash">yarn install
</code></pre>
<p>Puis on lance notre serveur de développement :</p>
<pre><code class="language-bash">yarn dev
</code></pre>
<p>Une URL va être générée. Il suffit de copier-coller cette URL dans l&#x27;application Lynx Explorer, et voilà !</p>
<h3>Notre premier écran</h3>
<p>Entrons dans le vif du sujet en effacant tout le rendu du composant <code>App</code>, et créons notre liste de posts :</p>
<div><div><div value="App.tsx">App.tsx</div><div value="components/PostItem.tsx">components/PostItem.tsx</div><div value="App.css">App.css</div><div value="hooks/usePosts.ts">hooks/usePosts.ts</div></div><div value="App.tsx"><pre><code class="language-tsx">import &quot;./App.css&quot;;
import PostItem from &quot;./components/PostItem.jsx&quot;;
import usePosts from &quot;./hooks/usePosts.js&quot;;

export function App() {
  const { data: posts, fetchNextPage } = usePosts();

return (

&lt;list
  className=&quot;safe-area posts-list&quot;
  list-type=&quot;single&quot;
  lower-threshold-item-count={2}
  bindscrolltolower={() =&gt; fetchNextPage()}
&gt;
  {posts?.pages?.flat().map((post) =&gt; {
    return (
      &lt;list-item key={post.id} item-key={post.id.toString()}&gt;
        &lt;PostItem body={post.body} id={post.id} title={post.title} /&gt;
      &lt;/list-item&gt;
    )
  })}
&lt;/list&gt;
); }

</code></pre></div><div value="components/PostItem.tsx"><pre><code class="language-tsx">type Props = {
  body: string;
  id: number;
  title: string;
};

const PostItem = ({ body, id, title }: Props) =&gt; {
  return (
    &lt;view className=&quot;post-item&quot;&gt;
      &lt;text className=&quot;post-item-title&quot;&gt;{title}&lt;/text&gt;
      &lt;text className=&quot;post-item-content&quot;&gt;{body}&lt;/text&gt;
    &lt;/view&gt;
  );
};

export default PostItem;

</code></pre></div><div value="App.css"><pre><code class="language-css">:root {
  background-color: #fefefe;
  --color-text: #000;
}

.safe-area {
  padding: env(safe-area-inset-top) env(safe-area-inset-right) env(safe-area-inset-bottom) env(
      safe-area-inset-left
    );
}

.posts-list {
  width: 100%;
  height: 100vh;
  list-main-axis-gap: 16px;
  padding-left: 16px;
  padding-right: 16px;
}

.post-item {
  display: flex;
  flex-direction: column;
  background-color: white;
  border-radius: 12px;
  box-shadow: 0px 4px 8px rgba(0, 0, 0, 0.2);
  padding: 16px;
  gap: 8px;
}

.post-item-title {
  font-size: 16px;
  font-weight: bold;
  color: var(--color-text);
}

.post-item-content {
  font-size: 14px;
  color: var(--color-text);
}
</code></pre></div><div value="hooks/usePosts.ts"><pre><code class="language-ts">import { useInfiniteQuery } from &#x27;@tanstack/react-query&#x27;

export type Post = {
  id: number
  title: string
  body: string
  userId: string
}

const usePosts = () =&gt; {
  return useInfiniteQuery({
    queryKey: [&#x27;posts&#x27;],
    queryFn: async ({ pageParam }) =&gt; {
      const response = await fetch(
        `https://jsonplaceholder.typicode.com/posts?_page=${pageParam}&amp;_limit=10`
      )
      const data = await response.json()
      return data as Post[]
    },
    getNextPageParam: (lastPage, allPages, lastPageParam) =&gt; {
      if (lastPage.length === 0) return undefined
      return lastPageParam + 1
    },
    initialPageParam: 1,
  })
}

export default usePosts
</code></pre></div></div>
<p>Et voilà, notre liste est prête. On note déjà plusieurs aspects qui diffèrent par rapport à React Native :</p>
<ul>
<li>pas d&#x27;import de composants (View, Text) : ils sont disponibles via le moteur de rendu (ici la librairie React)</li>
<li>possibilité d&#x27;utiliser des classes CSS. Il est aussi possible de mettre le style directement dans une prop <code>style</code> ou bien d&#x27;utiliser l&#x27;attribut <code>id</code>, comme en HTML classique</li>
</ul>
<p>L&#x27;élément <code>list</code> a un comportement qui se veut similaire à ce que propose la <a href="https://reactnative.dev/docs/flatlist">FlatList</a> de React Native. Cependant, le concept de recyclage des vues est ici totalement natif, là où React Native l&#x27;implémente du côté JS, même si plusieurs librairies alternatives tentent de résoudre ce problème (<a href="https://shopify.github.io/flash-list/">FlashList</a>, <a href="https://legendapp.com/open-source/list/intro/introduction/">Legend List</a>, <a href="https://github.com/azimgd/shadowlist">ShadowList</a>). L&#x27;API fournie pour la liste de Lynx est assez complète, rendant ainsi le système de layout très modulable. Par exemple, pour des listes multi colonnes, il est assez facile de faire qu&#x27;un sorte qu&#x27;un élément en particulier prenne toute la largeur disponible. La documentation fournit de <a href="https://lynxjs.org/api/elements/built-in/list.html">nombreux exemples présentant différents types de listes assez populaires</a>.</p>
<div type="info"><p>Les plus attentifs auront noté le nom de la prop <code>bindscrolltolower</code>, en particulier le préfixe
<code>bind</code>. Contrairement à React et React Native où les événements ont tendances à être préfixés par
un <code>on</code> (par exemple <code>onPress</code>), Lynx utilise <a href="https://lynxjs.org/guide/interaction/event-handling/event-propagation.html#event-handler-property">différents
préfixes</a>
en fonction de la manière dont vous souhaitez intercepter un événement.</p></div>
<h3>Détail d&#x27;un post</h3>
<p>Naturellement, nous souhaitons maintenant pouvoir cliquer sur un post pour afficher son détail ainsi que ses commentaires. Nous aurons donc besoin d&#x27;un système de navigation...inexistant ! En tout cas, ce système n&#x27;est pas fourni par Lynx, qui préconise plutôt <a href="https://lynxjs.org/react/routing.html">d&#x27;utiliser <code>React Router</code> en version 6</a>. Alors évidemment ça n&#x27;est pas idéal : contrairement à <a href="https://reactnavigation.org/">React Navigation</a> qui fournit des composants mappés sur des écrans natifs (donc avec des animations natives), on n&#x27;aura aucune animation de navigation, aucun composant natif de navigation (header, tab bar, etc) ou même de possibilité d&#x27;interactivité avec les gestures avec React Router. Nous allons tout de même utiliser <code>React Router</code> pour gérer la navigation entre les pages.</p>
<div><div><div value="index.tsx">index.tsx</div><div value="components/PostItem.tsx">components/PostItem.tsx</div><div value="App.css">App.css</div><div value="hooks/usePost.ts">hooks/usePost.ts</div></div><div value="index.tsx"><pre><code class="language-tsx">import { root } from &quot;@lynx-js/react&quot;;
import { QueryClient, QueryClientProvider } from &quot;@tanstack/react-query&quot;;
import { MemoryRouter, Route, Routes } from &quot;react-router&quot;;
import { App } from &quot;./App.js&quot;;
import Post from &quot;./Post.jsx&quot;;

const queryClient = new QueryClient();

root.render(

{&#x27; &#x27;}

&lt;QueryClientProvider client={queryClient}&gt;
  &lt;MemoryRouter&gt;
    &lt;Routes&gt;
      &lt;Route index element={&lt;App /&gt;} /&gt;
      &lt;Route path=&quot;/post/:id&quot; element={&lt;Post /&gt;} /&gt;
    &lt;/Routes&gt;
  &lt;/MemoryRouter&gt;
&lt;/QueryClientProvider&gt;
, );

if (import.meta.webpackHot) {
import.meta.webpackHot.accept();
}

</code></pre></div><div value="components/PostItem.tsx"><pre><code class="language-tsx">import { useNavigate } from &quot;react-router&quot;;

type Props = {
  body: string;
  id: number;
  title: string;
};

const PostItem = ({ body, id, title }: Props) =&gt; {
  const navigate = useNavigate();

  const onTap = () =&gt; {
    navigate(`/post/${id}`);
  };

  return (
    &lt;view className=&quot;post-item&quot; bindtap={onTap}&gt;
      &lt;text className=&quot;post-item-title&quot;&gt;{title}&lt;/text&gt;
      &lt;text className=&quot;post-item-content&quot;&gt;{body}&lt;/text&gt;
    &lt;/view&gt;
  );
};

export default PostItem;
</code></pre></div><div value="App.css"><pre><code class="language-css">.post-container {
  display: flex;
  flex-direction: column;
  gap: 16px;
  padding-left: 16px;
  padding-right: 16px;
}

.post-comment-container {
  display: flex;
  flex-direction: column;
  gap: 8px;
}

.post-comment-container:not(:last-of-type) {
  margin-bottom: 16px;
}

.post-comment-title {
  font-size: 18px;
  font-weight: bold;
  color: var(--color-text);
}

.post-comment-name {
  font-size: 14px;
  font-weight: 600;
  color: var(--color-text);
}

.post-comment-content {
  font-size: 14px;
  color: var(--color-text);
}
</code></pre></div><div value="hooks/usePost.ts"><pre><code class="language-ts">import { useQuery } from &#x27;@tanstack/react-query&#x27;
import type { Post } from &#x27;./usePosts.js&#x27;

type PostWithComments = Post &amp; {
  comments: Comment[]
}

type Comment = {
  id: string
  postId: string
  name: string
  email: string
  body: string
}

const usePost = (id: number) =&gt; {
  return useQuery({
    queryKey: [&#x27;post&#x27;, { id }],
    queryFn: async () =&gt; {
      const post = await fetch(
        `https://jsonplaceholder.typicode.com/posts/${id}?_embed=comments`
      ).then((response) =&gt; response.json())

      return post as PostWithComments
    },
  })
}

export default usePost
</code></pre></div></div>
<h3>Un peu d&#x27;animation ?</h3>
<p>Composante essentielle des applications mobiles, les animations donnent de la vie à nos écrans. En React Native, bien qu&#x27;une API d&#x27;animation soit disponible, on a tendance à utiliser la librairie <a href="https://docs.swmansion.com/react-native-reanimated/">Reanimated</a> qui, dans ses dernières versions, a sorti un support expérimental des animations CSS. Eh bien Lynx supporte cela par défaut, que ce soit via les feuilles de style CSS ou bien de manière impérative via une fonction appelée au runtime.</p>
<p>Ajoutons une animation CSS sur notre liste de posts :</p>
<pre><code class="language-css">@keyframes bounce {
  0% {
    transform: scale(1);
  }
  50% {
    transform: scale(1.02);
  }
  100% {
    transform: scale(1);
  }
}

.post-item {
  display: flex;
  flex-direction: column;
  background-color: white;
  border-radius: 12px;
  box-shadow: 0px 4px 8px rgba(0, 0, 0, 0.2);
  padding: 16px;
  gap: 8px;
  animation: bounce 2s ease-in-out infinite;
}
</code></pre>
<p>Animons maintenant un élément de la liste au moment d&#x27;un clic :</p>
<pre><code class="language-tsx">import { useNavigate } from &#x27;react-router&#x27;

type Props = {
  body: string
  id: number
  title: string
}

const PostItem = ({ body, id, title }: Props) =&gt; {
  const navigate = useNavigate()

  const onTap = () =&gt; {
    navigate(`/post/${id}`)
  }

  return (
    &lt;view
      className=&quot;post-item&quot;
      id={`post-${id}`}
      bindtap={() =&gt; {
        lynx.getElementById(`post-${id}`).animate(
          [
            {
              transform: &#x27;rotate(20deg)&#x27;,
              &#x27;animation-timing-function&#x27;: &#x27;linear&#x27;,
            },
            {
              transform: &#x27;rotate(0deg)&#x27;,
              &#x27;animation-timing-function&#x27;: &#x27;linear&#x27;,
            },
            {
              transform: &#x27;rotate(-20deg)&#x27;,
              &#x27;animation-timing-function&#x27;: &#x27;linear&#x27;,
            },
            {
              transform: &#x27;rotate(0deg)&#x27;,
              &#x27;animation-timing-function&#x27;: &#x27;linear&#x27;,
            },
          ],
          {
            name: &#x27;shake-rotate-anim&#x27;,
            duration: 1000,
            iterations: 1,
            easing: &#x27;ease-in-out&#x27;,
          }
        )
      }}
    &gt;
      &lt;text className=&quot;post-item-title&quot;&gt;{title}&lt;/text&gt;
      &lt;text className=&quot;post-item-content&quot;&gt;{body}&lt;/text&gt;
    &lt;/view&gt;
  )
}

export default PostItem
</code></pre>
<h2>Compatibilité Web</h2>
<p>Lynx propose une compatibilité web par défaut, mais sa mise en place est un peu plus complexe que ce que propose Expo par exemple.</p>
<p>Tout d&#x27;abord il faut modifier notre fichier <code>lynx.config.ts</code> à la racine :</p>
<pre><code class="language-ts">import { defineConfig } from &#x27;@lynx-js/rspeedy&#x27;

import { pluginQRCode } from &#x27;@lynx-js/qrcode-rsbuild-plugin&#x27;
import { pluginReactLynx } from &#x27;@lynx-js/react-rsbuild-plugin&#x27;

export default defineConfig({
  plugins: [
    pluginQRCode({
      schema(url) {
        // We use `?fullscreen=true` to open the page in LynxExplorer in full screen mode
        return `${url}?fullscreen=true`
      },
    }),
    pluginReactLynx(),
  ],
  environments: {
    web: {
      output: {
        assetPrefix: &#x27;/&#x27;,
      },
    },
    lynx: {},
  },
})
</code></pre>
<p>Puis on crée un bundle :</p>
<pre><code class="language-sh">yarn build
</code></pre>
<p>On crée ensuite un projet <code>rsbuild</code> à la racine de notre projet Lynx :</p>
<pre><code class="language-sh">yarn create rsbuild
</code></pre>
<p>Dans notre nouveau projet, on install de nouvelles dépendances :</p>
<pre><code class="language-sh">yarn add @lynx-js/web-core @lynx-js/web-elements
</code></pre>
<p>On édite ensuite le fichier <code>src/App.tsx</code> :</p>
<pre><code class="language-tsx">import &#x27;@lynx-js/web-core/index.css&#x27;
import &#x27;@lynx-js/web-elements/index.css&#x27;
import &#x27;@lynx-js/web-core&#x27;
import &#x27;@lynx-js/web-elements/all&#x27;

const App = () =&gt; {
  return &lt;lynx-view style={{ height: &#x27;100vh&#x27;, width: &#x27;100vw&#x27; }} url=&quot;/main.web.bundle&quot;&gt;&lt;/lynx-view&gt;
}

export default App
</code></pre>
<p>On édite ensuite le fichier <code>rsbuild.config.ts</code> :</p>
<pre><code class="language-ts">import { defineConfig } from &#x27;@rsbuild/core&#x27;
import { pluginReact } from &#x27;@rsbuild/plugin-react&#x27;
import path from &#x27;node:path&#x27;
import { fileURLToPath } from &#x27;node:url&#x27;
const __filename = fileURLToPath(import.meta.url)
const __dirname = path.dirname(__filename)

export default defineConfig({
  plugins: [pluginReact()],
  server: {
    publicDir: [
      {
        name: path.join(__dirname, &#x27;..&#x27;, &#x27;dist&#x27;),
      },
    ],
  },
})
</code></pre>
<p>Puis on lance notre serveur de développement :</p>
<pre><code class="language-sh">yarn dev
</code></pre>
<p>L&#x27;app est disponible sur <code>http://localhost:3000</code>. En inspectant le HTML rendu, on remarque une structure peu conventionnelle, remplie de web components, et très peu de tags natifs. Notre animation au clic implémentée précédemment ne fonctionne pas non plus. On a ce sentiment que le support web n&#x27;est pas vraiment au point, mais il est présent !</p>
<h2>Performances</h2>
<p>Côté performances, Lynx est assez similaire à ce que propose React Native, mais propose une fonctionnalité supplémentaire : l&#x27;<a href="https://lynxjs.org/guide/interaction/ifr.html">Instant First-Frame Rendering</a>, qui est très semblable au concept de SSR que l&#x27;on connaît dans l&#x27;univers de React. L&#x27;IFR permet d&#x27;afficher le premier écran de notre application de manière quasi instantanée.</p>
<p>Architecturalement parlant, Lynx utilise, tout comme React Native, deux threads. Il est possible de choisir sur quel thread exécuter nos fonctions grâce aux <a href="https://lynxjs.org/api/react/document.directives#main-thread">directives</a> <code>&#x27;background only&#x27;</code> et <code>&#x27;main thread&#x27;</code> en fonction du contexte d&#x27;utilisation. Par exemple pour une requête asynchrone comme l&#x27;envoi d&#x27;un formulaire, on privilégiera la directive <code>&#x27;background only&#x27;</code> afin de ne pas bloquer le thread principal. Pour une fonction qui demande un résultat le plus rapidement possible, on privilégiera la directive <code>&#x27;main thread&#x27;</code>. Ce concept n&#x27;est pas sans rappeler les <a href="https://docs.swmansion.com/react-native-reanimated/docs/guides/worklets">worklets</a> de Reanimated, permettant l&#x27;exécution de fonctions dans le thread UI ou d&#x27;autres threads.</p>
<h2>Conclusion</h2>
<p>Bien que prometteur sur bien des aspects, Lynx est à l&#x27;heure actuelle encore trop jeune pour être réellement utilisé de la même manière que l&#x27;on pourrait utiliser React Native. On a ce sentiment que la librairie a été dévelopée pour être intégrée à une app existante, et non pour créer une application complète. Beaucoup d&#x27;API assez essentielles sont manquantes par rapport à ce que l&#x27;on peut trouver en React Native. J&#x27;ai personnellement été surpris de voir que Lynx ne supporte l&#x27;élément <code>input</code> que par l&#x27;application Lynx Explorer, sans qui on ne pourrait tout simplement pas avoir de champ de saisie.</p>
<p>Tous ces éléments ont déjà été remontés par la communauté et seront déployés petit à petit dans <a href="https://lynxjs.org/blog/lynx-open-source-roadmap-2025.html">les prochaines releases de 2025</a>. Pour le moment, il est encore trop tôt pour se lancer dans la conception d&#x27;une application, mais nous allons tout de même garder un oeil sur l&#x27;évolution de la librairie.</p>]]></content:encoded>
            <enclosure url="https://www.premieroctet.com/blog/lynx-remplacant-de-react-native/illu.png" length="0" type="image/png"/>
        </item>
        <item>
            <title><![CDATA[View Transition API et son intégration dans NextJS]]></title>
            <link>https://www.premieroctet.com/blog/view-transition-api-dans-next-js-et-react</link>
            <guid>https://www.premieroctet.com/blog/view-transition-api-dans-next-js-et-react</guid>
            <pubDate>Tue, 15 Apr 2025 00:00:00 GMT</pubDate>
            <description><![CDATA[Découvrez comment NextJS intègre l’API View Transition de React pour créer des animations CSS fluides. Une approche expérimentale, qui simplifie les transitions sans librairie tierce. Comprendre cette nouveauté change la donne côté UI]]></description>
            <content:encoded><![CDATA[<p>L’API <em>View Transition</em> permet d’animer de manière native les changements d’état d’une page, sans dépendre de bibliothèques tierces. Récemment, Next.js a intégré cette fonctionnalité de manière expérimentale.</p>
<p>Bien que cette approche soit encore en phase de test et peu documentée,  elle ouvre la voie à une simplification des animations que l’on considérait jusqu’ici comme complexes.</p>
<p>Avant d’aborder son utilisation dans Next.js, je vais rapidement vous présenter l’API dans sa version native pour celles et ceux qui ne seraient pas encore familiers avec elle.
Si vous êtes déjà familier avec l’API, vous pouvez directement passer à <a href="#activer-la-magie-des-transitions-dans-nextjs">cette section</a>.</p>
<h2>Qu&#x27;est-ce que l&#x27;API <em>View Transition</em> ?</h2>
<p>L&#x27;API <a href="https://developer.mozilla.org/en-US/docs/Web/API/View_Transition_API"><em>View Transition</em></a> est une fonctionnalité native du navigateur qui permet d’animer les changements d’états du DOM (ou de navigation) sans avoir recours à une librairie tierce comme <a href="https://motion.dev/">Framer Motion</a> ou <a href="https://gsap.com/">GSAP</a>.
Elle simplifie grandement l’intégration de transitions / animations visuelles lors de la mise à jour du contenu d’une page.</p>
<div type="warning"><p>Cette fonctionnalité n’est pas supportée par tous les navigateurs. Pour l’instant seuls les navigateurs basés sur Chromium, et le navigateur Safari la supportent.</p></div>
<h3>startViewTransition</h3>
<p>Le cœur de cette API repose sur la méthode <code>startViewTransition</code>.</p>
<pre><code class="language-js">document.startViewTransition(() =&gt; updateTheDOMSomehow())
</code></pre>
<p>Appeler cette méthode, en lui passant en callback une fonction qui met à jour le DOM, déclenche un cycle de <em>view transition</em>.</p>
<p>Qu&#x27;est-ce que ça veut dire ? Essentiellement, à l&#x27;appel de cette méthode, l&#x27;API capture l&#x27;état de la page. Une fois l&#x27;opération terminée (la capture de la page) c&#x27;est votre fonction de mise à jour du DOM qui est appelée et l&#x27;API capture ensuite de nouveau l&#x27;état de la page, après votre mutation du DOM.</p>
<p>L’API construit ensuite une arborescence qui ressemble à celle-ci :</p>
<pre><code class="language-bash">::view-transition 
└─ ::view-transition-group(root)
   └─ ::view-transition-image-pair(root)
      ├─ ::view-transition-old(root)
      └─ ::view-transition-new(root)
</code></pre>
<p>Comme son nom l’indique <code>::view-transition-old</code> représente la capture de l’ancienne vue et, vous l’aurez compris, <code>::view-transition-new</code> la capture actuelle de la vue.</p>
<p><img src="https://www.premieroctet.com/blog/view-transition-api-dans-next-js-et-react/chrome-dev-tools-view.png" alt="Les pseudo-élements ::view-transition dans la devtool Chrome" title="Les pseudo-élements ::view-transition dans la devtool Chrome"/></p>
<p>L’ancienne vue est animée en <em>fade out</em> tandis que la nouvelle apparaît en <em>fade in</em> (via la propriété CSS <code>opacity</code>). C’est le comportement par défaut, mais il peut bien sûr être customisé (c&#x27;est là tout l&#x27;intérêt).</p>
<h3>Customiser ses transitions</h3>
<p>Pour customiser les <em>view transitions</em> on utilisera les pseudo selecteurs <code>::view-transition....</code> en CSS.</p>
<p>Ainsi, si je souhaite faire une transition un peu plus complexe (qu&#x27;un <em>fade in / out</em>) je peux cibler ma transition de vue et y appliquer une animation CSS de mon choix.</p>
<p>Par exemple, ici, j’applique respectivement mes animations <code>pop-in</code> et <code>pop-out</code> que j’aurai définies plus haut dans ma feuille de style.</p>
<pre><code class="language-css">::view-transition-old(root) {
  animation: pop-out 0.3s ease;
}

::view-transition-new(root) {
  animation: pop-in 0.3s ease;
}
</code></pre>
<h3>Cibler un élément précis</h3>
<p>Dans l’exemple précédent, nous avons utilisé un <em>view transition</em> sur l’ensemble de la page (<code>root</code>) mais il est possible de cibler un élément précis.</p>
<p>Pour ce faire, on doit d’abord lui donner un nom de vue de transition (<code>view-transition-name</code>).</p>
<pre><code class="language-css">.box {
  view-transition-name: box;
}
</code></pre>
<p>On pourra ensuite cibler la vue de transition liée à cet élément avec les pseudo éléments correspondants:</p>
<pre><code class="language-css">::view-transition-old(box) {
	animation: skew-out 0.3s ease;
}

::view-transition-new(box) {
  animation: skew-in 0.3s ease;
}
</code></pre>
<div type="warning"><p>Vous devez attribuer un <code>view-transition-name</code> unique à chaque élément pour que la capture fonctionne.
Si vous souhaitez animer plusieurs éléments de la même manière vous pouvez utiliser la propriété <code>view-transition-class: myClass</code> dans ce cas il faudra préfixer le nom de votre classe par un point, pour le sélectionner avec le pseudo-élément (ex: <code>view-transition-old(.myClass)</code> ou <code>view-transition-new(.myClass)</code>).</p></div>
<h3>Deux types de transitions</h3>
<p>Les transitions de vues se découpent en deux catégories.</p>
<p>Les transitions de vues sur <strong>le même document</strong> et celles sur <strong>multi-documents</strong> (transition de changement de page).
Les deux catégories reposent sur les mêmes principes à la différence que pour une transition multi-documents on n’a pas besoin d’appeler la méthode <code>startViewTransition</code> pour démarrer la transition.
C’est la navigation entre les documents qui déclenchera la transition.</p>
<p>Nous voilà globalement à jour sur l’état de l’API <em>View Transition</em> (au jour de la rédaction de cet article).</p>
<p>Si vous souhaitez approfondir plus en détails l’API je vous recommande la documentation <a href="https://developer.mozilla.org/en-US/docs/Web/API/View_Transition_API">MDN</a> (quoiqu&#x27;un peu légère), <a href="https://www.w3.org/TR/css-view-transitions-1/#idl-index">la spécification W3C</a> et <a href="https://developer.chrome.com/s/results?hl=fr&amp;q=View%20transition%20API#gsc.tab=0&amp;gsc.q=View%20transition%20API&amp;gsc.sort=">les nombreux articles de Chrome For Developpers</a> sur le sujet.</p>
<h2>Activer la magie des transitions dans Next.js</h2>
<p>Récemment, une fonctionnalité a été introduite pour faciliter l’intégration de l&#x27;API <em>View Transition</em> dans les projets Next.js. Mais attention, on entre ici dans un territoire <strong>très peu documenté</strong>.</p>
<p>Cette fonctionnalité, apparue lors d&#x27;<a href="https://github.com/vercel/next.js/pull/74659">une release</a> il y a quelques mois, est encore marquée comme <code>experimental</code>. À l’heure où j’écris ces lignes, la documentation officielle est assez minimale sur le sujet (<a href="https://nextjs.org/docs/app/api-reference/config/next-config-js/viewTransition">voici le lien si vous voulez jeter un œil</a>) et les ressources restent rares.</p>
<p>C’est justement ce manque de documentation qui m’a poussé à creuser un peu.</p>
<p>En fouillant un peu, je suis tombé sur <a href="https://github.com/delbaoliveira/next-view-transitions">une démo</a> partagée par <a href="https://github.com/delbaoliveira">Delba Oliveira</a>, développeuse chez Vercel.
Ça m’a donné envie d’approfondir le sujet et d’expérimenter directement avec cette nouvelle API dans un petit projet de test de mon côté. Voici ce que j&#x27;en ai retiré.</p>
<p><em>Spoiler : c’est déjà prometteur.</em></p>
<div type="info"><p>Pour utiliser cette fonctionnalité dans NextJS il faut activer un flag experimental : <code>viewTransition</code>. Et utiliser une version ≥ <code>v15.2.0</code>.</p><pre><code class="language-diff-js:next.config.js">module.exports = {
  // ...
  experimental: {
    viewTransition: true,
  },
}
</code></pre></div>
<h3>Le composant <code>unstable_viewTransition</code></h3>
<p>Actuellement, React expose un composant <code>unstable_viewTransition</code> (oui, le nom annonce la couleur). Il prend plusieurs propriétés :</p>
<ul>
<li><code>name</code> : L&#x27;équivalent de <code>view-transition-name</code> en CSS.</li>
<li><code>className</code>: Pour assigner une <a href="https://developer.chrome.com/blog/view-transitions-update-io24#view-transition-class"><code>view-transition-class</code></a> à l&#x27;élément.</li>
<li><code>exit</code> / <code>enter</code> : Pour ajouter une classe (CSS) à l&#x27;animation de sortie ou d&#x27;entrée de l&#x27;élément. (Au montage ou au démontage du composant)</li>
<li>Et d&#x27;autres propriétés que je n&#x27;ai pas encore explorées mais qui sont typées et documentées dans les types de React.</li>
</ul>
<p>Concrètement, voici à quoi ça peut ressembler dans le code :</p>
<pre><code class="language-jsx">import { unstable_viewTransition as ViewTransition } from &#x27;react&#x27;

export default function MyComponent() {
  return (
    &lt;ViewTransition name=&quot;box&quot;&gt;
      &lt;div className=&quot;box&quot;&gt;Hello&lt;/div&gt;
    &lt;/ViewTransition&gt;
  )
}
</code></pre>
<div type="info"><p>Même si l’article parle ici d’intégration dans Next il est important de noter qu’il s’agit à la base d’une fonctionnalité de React introduite par cette <a href="https://github.com/facebook/react/pull/31975">pull request</a>.
C’est la raison pour laquelle on importe le composant <code>unstable_ViewTransition</code> de React.</p></div>
<h3>Une démonstration</h3>
<p>Et maintenant… <a href="https://next-js-view-transition-demo.vercel.app/blog">une petite demo maison</a> pour montrer ce que ça donne en action dans un mini blog NextJS 👇</p>
<div class="u-txt-center" style="margin-top:20px"><video class="b-lazy b-loaded" autoplay="" loop="" muted="" title="Next JS View Transition Demo"><source src="https://www.premieroctet.com/blog/view-transition-api-dans-next-js-et-react/next-view-transition-demo.webm" type="video/webm; codecs=vp9,vorbis"/><source src="https://www.premieroctet.com/blog/view-transition-api-dans-next-js-et-react/next-view-transition-demo.mp4" type="video/mp4"/></video></div>
<p>Dans cette démo on utilise la transition de vue multi-documents, c’est donc la navigation qui déclenche les transitions.
J&#x27;ai donné le même <code>name</code> au composant <code>&lt;ViewTransition&gt;</code> utilisé dans la page <code>/blog</code> et la page <code>/blog/post/[slug]</code>.
Donc à la navigation entre les deux pages, la transition est appliquée pour animer le changement de page entre ces deux éléments.</p>
<div type="info"><p>Hormis un petit effet de flou sur les images, cette démo est purement réalisée avec l&#x27;API de React, en mode par défaut, tel que je l&#x27;ai présenté plus haut, sans configuration supplémentaire.
C&#x27;est donc donc assez simple à mettre en place.</p></div>
<p>Et voici <a href="https://github.com/quentingrchr/next-js-view-transition-demo">le code source</a> utilisé pour cetté démo.</p>
<h2>Limites et perspectives</h2>
<p>L’intégration expérimentale de l&#x27;API <em>View Transition</em> au sein de Next.js ouvre des perspectives intéressantes pour le développement d’interfaces plus fluides.
À l’avenir on pourrait imaginer la configurations de transitions prédéfinies entre les pages à l’instar de <a href="https://nuxt.com/docs/getting-started/transitions">NuxtJS</a>.</p>
<p>Néanmoins, comme je l’ai rappelé plusieurs fois tout au long de cet article c’est une fonctionnalité qui n’est encore pas totalement prête pour un usage en production, l’API pourrait changer et sera sûrement encore sujette à évolution.</p>
<h2>Bonus : Utilisations créatives de l&#x27;API <em>View Transition</em></h2>
<p>En bonus, j&#x27;ai rassemblé quelques exemples de sites qui utilisent l&#x27;API <em>View Transition</em> de manière créative pour que vous ayez une idée des possibilités qu&#x27;elle ouvre.</p>
<ul>
<li>https://nmn.sh/</li>
<li>https://cydstumpel.nl/</li>
<li>https://x.com/delba_oliveira/status/1897701817431044124</li>
<li>https://theme-toggle.rdsx.dev/</li>
<li>https://framer-ground-svelte.vercel.app/layout/page-wipe</li>
</ul>
<h2>Ressources</h2>
<ul>
<li><a href="https://developer.mozilla.org/en-US/docs/Web/API/View_Transition_API">MDN View Transition API</a></li>
<li><a href="https://www.julienpradet.fr/tutoriels/view-transitions/">Comment utiliser l&#x27;API View Transitions ? Du hello world aux cas complexes.</a></li>
<li><a href="https://github.com/delbaoliveira/next-view-transitions">NextJS View Transition Demo</a></li>
<li><a href="https://nextjs.org/docs/app/api-reference/config/next-config-js/viewTransition">NextJS Documentation View Transition</a></li>
<li><a href="https://developer.chrome.com/docs/web-platform/view-transitions/same-document">Transitions d&#x27;affichage pour un même document dans les applications monopages</a></li>
<li><a href="https://developer.chrome.com/docs/web-platform/view-transitions">Transitions fluides avec l&#x27;API View Transition</a></li>
</ul>]]></content:encoded>
            <enclosure url="https://www.premieroctet.com/blog/view-transition-api-dans-next-js-et-react/illu.png" length="0" type="image/png"/>
        </item>
    </channel>
</rss>