<?xml version="1.0" encoding="utf-8"?><rss version="2.0">
  <channel>
    <title>Blog Liip</title>
    <link>https://www.liip.ch/fr/blog</link>
    <lastBuildDate></lastBuildDate>
            <item>
      <title>Iframes are still odd</title>
      <link>https://www.liip.ch/fr/blog/iframes-are-still-odd</link>
      <guid>https://www.liip.ch/fr/blog/iframes-are-still-odd</guid>
      <pubDate>Mon, 23 Mar 2026 00:00:00 +0100</pubDate>
      <description><![CDATA[<h2>The Challenge</h2>
<p>The application does one - rather complicated - task, with lots of business logic. There was no way we could rewrite it to include it directly into the website code. And because the application comes with its own Javascript and CSS, we decided to use an iframe to embed the application with clean isolation.</p>
<p>The company maintaining that application provided us with a version - running as a Docker container - where they had stripped all extra elements like the navigation, so that it would visually fit within the website. There was however no way for us to customise anything within the application.</p>
<h2>iframe security</h2>
<p>The promise of an iframe is to keep a clean security boundary between embedding page and embedded content. This means that it is by design not possible to call Javascript across the boundary. </p>
<p>Because injecting iframes could potentially trick a user into submitting data to an attacker (clickjacking), as the iframe may be from a different origin than the main page. Thus, to even render the iframe, the browser checks the Content-Security-Policy (CSP) HTTP header. That header has a field frame-src to control what may be included as an iframe. With this, I allowed the domain of the application to be included in iframes.</p>
<pre><code>Content-Security-Policy: frame-src https://my-embed.com;</code></pre>
<p>But not only does the including page need to allow an iframe. The page to be embedded also needs to allow being included with the frame-ancestors attribute of the CSP header. As we run the application Docker image under our control, I was able to add that header in the proxy that runs before the Docker image:</p>
<pre><code>Content-Security-Policy: frame-ancestors https://my-website.com;</code></pre>
<p>Several things to note:</p>
<ul>
<li>If you have other CSP rules, merge them with the rules, nginx will overwrite the header and not add to it</li>
<li>Both options also support "self" to allow embedding resp. being embedded with the same webserver</li>
<li>Prior to the CSP becoming a standard, there was an unofficial header <code>X-Frame-Options</code>, which is still supported by browsers</li>
<li><code>Content-Security-Policy</code> must be an actual HTTP header, <code>&lt;meta http-equiv=”...”&gt;</code> is ignored for <code>Content-Security-Policy</code> (and also ignored for <code>X-Frame-Options</code>).</li>
</ul>
<h2>Size of the iframe element</h2>
<p>Now we get to the weird parts. To prevent multiple scrollbars, we need the iframe element to be exactly big enough for the embedded page. If it is too small, there is an additional scrollbar (or hidden content). If it is too large, there is odd whitespace.</p>
<p>The size of the element needs to be set on the iframe, owned by the parent. The dimensions of the content are however only known by the embedded application. HTML / CSS do not provide any means to let the parent page declare that it wants the iframe to have the “necessary size”. </p>
<p>We ended up with a really convoluted way, which seems the only way to achieve this: sending messages from the child page to the parent. This problem spawned dedicated javascript libraries like <a href="https://github.com/davidjbradshaw/iframe-resizer">iframe-resizer</a>. We ended up reimplementing the logic in the React application, as it was so small that a dedicated library felt like overkill. Following the tutorial at <a href="https://github.com/craigfrancis/iframe-height/">iframe-height</a> (which also has some interesting background on the discussion about iframes in the Whatwg), we came up with this code for the containing website:</p>
<pre><code class="language-js">// register an event listener for messages
window.addEventListener('message', receiveMessage, false);

// handle a message
function receiveMessage(event) {
    const origin = event.origin || event.originalEvent.origin;
    // we configure the expected domain to allow for this additional sanity check
    if (expectedDomain !== origin) {
      return;
    }
    if (!event.data.request || 'iframeResize' !== event.data.request) {
      return;
    }
    // the id is known in the js class. we need to find the element that needs to be resized
    const iframe = document.getElementById(`iframe-${id}`);
    if (iframe) {
      // pad the height a bit to avoid unnecessary tiny scrolling
      iframe.style.height = (event.data.height + 20) + 'px';
      // width could be handled the same way if necessary - in our case the width is fix
    }
}</code></pre>
<p>Now we need to make the embedded content send a message with its height. The Javascript for that is a bit verbose to allow for different browsers, but not complicated either:</p>
<pre><code class="language-js">(
    function(document, window) {
      if (undefined === parent || !document.addEventListener) {
        return;
      }
      function init() {
        let owner = null;
        const width = Math.max(document.body.scrollWidth, document.body.offsetWidth, document.documentElement.clientWidth, document.documentElement.scrollWidth, document.documentElement.offsetWidth);
        const height = Math.max(document.body.scrollHeight, document.body.offsetHeight, document.documentElement.clientHeight, document.documentElement.scrollHeight, document.documentElement.offsetHeight);
        if (parent.postMessage) {
          owner = parent;
        } else if (parent.contentWindow &amp;&amp; parent.contentWindow.postMessage) {
          owner = parent.contentWindow;
        } else {
          return;
        }
        owner.postMessage({
          'request' : 'iframeResize',
          'width' : width,
          'height' : height
        }, '*');
      }

      if (document.readyState !== 'loading') {
        window.setTimeout(init);
      } else {
        document.addEventListener('DOMContentLoaded', init);
      }
      // this is needed to also adjust the iframe if something (e.g. the Javascript of the application) changes the size of the iframe without an actual page reload.
      const observer = new ResizeObserver(init);
      observer.observe(document.body);
    }
  )(document, window);"</code></pre>
<h3>iframes with same origin</h3>
<p>If the iframe comes from the same origin (= domain) as the parent page, Javascript can cross the boundary. From parent to child, there is a <code>contentWindow</code> property on the <code>iframe</code> element. From child to parent, there is <code>window.parent</code>. With same origin, those elements expose all things the window usually has. For different origins, they only expose the function <code>postMessage</code> for the secure separation.</p>
<h2>Injecting content with Nginx</h2>
<p>Remember how I said we can’t modify the application? That still holds true. If we would have loaded both applications from the same domain, we could have had the parent page add a listener inside the iframe to directly update dimensions as needed. But the application contained absolute paths for its assets, so providing it from a subfolder of the same domain would have been tricky and we had to run it on a separate domain. </p>
<p>I ended up injecting the above snippet of Javascript in the Nginx proxy that sits in front of the Docker container:</p>
<pre><code>proxy_set_header Accept-Encoding ""; # make sure we get plain response for substitution to work
...
sub_filter_last_modified on;
sub_filter "&lt;/body&gt;" "&lt;script language='javascript'&gt;${script}&lt;/script&gt;&lt;/body&gt;";
sub_filter_once on;
...
proxy_pass https://my-embed.com$request_uri;</code></pre>
<p>Now the embedded iframe communicates its size to the containing page, which adjusts the iframe size accordingly.</p>
<p>(Note that Nginx does not execute these statements in order. The sub_filter instructions apply to the response, wihle proxy_set_header and proxy_pass apply to the request.)</p>
<h2>Alternatives</h2>
<p>Web Components are a more lightweight solution to combine separate sources into one website. If what you need to integrate is just an element and not a whole application, they might be a better fit. My collegue Falk wrote about <a href="https://www.liip.ch/en/blog/web-components-the-good-the-bad-and-the-ugly">Web Components</a> last week.</p>
<hr />
<h2>Bonus: Access control for the iframe content</h2>
<p>Because the application is not under our control, we need to manage access to it. We told the supplier of the application to remove access control and simply allow items to be created and edited by ID. Of course, this means that the application must never be directly exposed to the internet, but only reachable through the proxy.</p>
<p>On the embedding side, we track which user is allowed what ids, and have Nginx do a pre-flight check against the website to get the access decision:</p>
<pre><code># at the beginning of the location for the main request to the embeded application
auth_request /auth;

location /auth {
    # preflight authorization request with symfony
    fastcgi_pass phpfcgi;
    include /usr/local/openresty/nginx/conf/fastcgi_params;
    fastcgi_param SCRIPT_FILENAME /app/public/index.php;
    # forward the original request URI to allow our main application to verify access to the specific resource
    fastcgi_param REQUEST_URI /embed-check$request_uri;
    # body is not forwarded. we have to remove content length separately, otherwise PHP-FPM will wait for the body until the auth request times out.
    fastcgi_pass_request_body off;
    fastcgi_param CONTENT_LENGTH "";
    fastcgi_param CONTENT_TYPE "";
    internal;
}</code></pre>
<p>If the call at <code>/embed-check/...</code> returns a 2xx status, Nginx continues with the request, otherwise it returns the response with the status code to the client, allowing for example to redirect to the login page. In my case, i return an empty response with status 204 if the user is allowed to access the specific resource.</p>
<p>On Symfony side, I use Symfony security to make sure the user is logged in. And then parse the path to know which item in the application the request wants to access, and check if the user has access. This leaks knowledge about the URL design of the embedded application, which is not avoidable for granular access control.</p>]]></description>
    </item>
        <item>
      <title>Preventing Context Pollution for AI Agents</title>
      <link>https://www.liip.ch/fr/blog/preventing-context-pollution-for-ai-agents</link>
      <guid>https://www.liip.ch/fr/blog/preventing-context-pollution-for-ai-agents</guid>
      <pubDate>Wed, 18 Mar 2026 00:00:00 +0100</pubDate>
      <description><![CDATA[<p>Context pollution happens when the context window fills up with information that is irrelevant to the current task. The more an agent has to juggle, the more likely it loses track of what it was actually doing.</p>
<p>Here are practical techniques to prevent it.</p>
<h2>Session Hygiene</h2>
<p>Start a fresh session for each task. This is the simplest technique and the easiest to get right. If earlier research is needed, write it into a temporary handoff file and let a new session pick up from there.</p>
<h2>Streamline Tool Calling</h2>
<p>Every tool call adds tokens to the context. Poorly built tools add a lot of them. To keep the context lean:</p>
<ul>
<li>Choose tools and MCPs that are well built and optimize token usage</li>
<li>Make sure via prompting that the right tools are used from the start</li>
</ul>
<p>A single bloated tool response can waste more context than an entire conversation turn.</p>
<h2>Subagents</h2>
<p>Agents can spawn other agents that run in their own context. This isolates work and keeps the parent context clean. It helps most when building large features where individual parts are independent.</p>
<p>The easiest way to use subagents is to prompt something like:</p>
<pre><code>Split the current plan into tasks, use a subagent for each task.</code></pre>
<h2>Persistent Tasks</h2>
<p>I built an MCP for Claude Code called <code>deliverables-mcp</code> that lets an agent create persistent tasks per codebase. Tasks are stored in <code>.claude/deliverables.jsonl</code> and persist across sessions.</p>
<p>This allows:</p>
<ul>
<li>Starting a new session before implementing each task</li>
<li>Running subagents in parallel based on tasks dependencies</li>
<li>Restarting a failed task in a clean session</li>
</ul>
<p>The tool replaces Claude Code's internal tasks and is deliberately called "deliverables" for two reasons:</p>
<ol>
<li>To avoid confusing the agent with two tools both called "tasks"</li>
<li>Deliverables are typically larger than just a task, which is a sweet spot for AI agents. Not so small that handoff cost dominates, but small enough that context problems are rare.</li>
</ol>
<p>You can check out <code>deliverables-mcp</code> on <a href="https://github.com/FalkZ/deliverables-mcp">GitHub</a>.</p>]]></description>
    </item>
        <item>
      <title>Le chatbot ConfIAnce, un an apr&#232;s</title>
      <link>https://www.liip.ch/fr/blog/le-chatbot-confiance-un-an-apres</link>
      <guid>https://www.liip.ch/fr/blog/le-chatbot-confiance-un-an-apres</guid>
      <pubDate>Tue, 17 Mar 2026 00:00:00 +0100</pubDate>
      <description><![CDATA[<p>Il y a un peu moins d’un an, nous vous avions présenté <a href="https://www.liip.ch/fr/blog/confiance-premier-chatbot-llm-de-medecine-generale-en-suisse">le chatbot ConfIAnce</a>. Mandaté par les HUG, nous avons développé ce robot conversationnel dont l’objectif est de proposer un accès facilité et interactif aux informations médicales produites sur les maladies chroniques courantes et validées par l’établissement médical. </p>
<p>Un article publié par les initiateurs et initiatrices de ce projet dans le dernier numéro de la Revue Médicale Suisse dresse un bilan provisoire, un an après la mise en ligne publique.</p>
<figure><img alt="" src="https://liip.rokka.io/www_inarticle_5/9661ee/rms-confiance.jpg" srcset="https://liip.rokka.io/www_inarticle_5/o-dpr-2/9661ee/rms-confiance.jpg 2x"></figure>
<h2>Un chatbot officiel plutôt que des réponses parfois erronées sur le net</h2>
<p>La médecine de premier recours, essentielle au bon fonctionnement du système de santé, fait face à une pénurie croissante, même en ville. Faute d’accès à leur médecin traitant, les patients sont incités à rechercher des réponses sur internet, qui sont souvent erronées voire dangereuses. </p>
<p>Dans ce contexte, une solution IA bien pensée pourrait apporter <strong>la bonne information au bon moment</strong>.</p>
<p>C’est pourquoi nous avons accompagné les HUG à développer un chatbot de type <strong>RAG (Retrieval Augmented Generation)</strong>. ConfIAnce n’est pas le premier chatbot destiné aux patients. Cependant, il se distingue par “son ancrage institutionnel, l’utilisation de contenus médicaux validés localement, et la mise en place de couches de contrôle pour garantir la fiabilité des réponses”. </p>
<p>Pour garantir la sécurité, il intègre des systèmes de contrôle rigoureux: <strong>matching, groundedness, harmfulness, tests automatiques, routage sémantique</strong>.</p>
<h2>Garder le contrôle de l’outil pour assurer la qualité</h2>
<p>L’un des enjeux est de bien garder le contrôle de l’outil, ce qui nécessite des fonctionnalités de surveillance du chatbot. Pour ce faire, des tests automatisés sont effectués sur l’ensemble des réponses apportées aux questions posées. On y mesure la cohérence factuelle de la réponse générée par le chatbot par rapport au contenu de la base de connaissances (faithfullness).</p>
<p>De plus, un routage, ajustable par les administrateurs, garantit un contrôle humain en filtrant et en dirigeant les questions de manière appropriée. Les administrateurs peuvent aussi mettre le chatbot immédiatement hors ligne en cas de suspicion de dysfonctionnement.<br />
Les thèmes peu abordés dans les documents sources et qui sont pourtant un sujet de questions vont aussi être développés pour enrichir la base de connaissances dans <strong>un processus d’amélioration continue</strong>. </p>
<h2>Un tel outil, aussi performant soit-il, n’est utile que s’il est adopté</h2>
<p>Pour que le chatbot soit utilisé par les patients, les HUG ont mené une campagne d’information publique. L’objectif était de promouvoir l’outil, tout en restant réaliste quant aux attentes des patient·e·s. </p>
<p>Ce n’est <strong>pas un dispositif médical qui se substitue à une visite</strong>, mais bien un soutien informationnel permettant de répondre à des questions portant sur les maladies chroniques les plus courantes chez les adultes.</p>
<p>En février 2025, ConfIAnce sort en version beta. Entre début février et fin octobre 2025, 3'823 utilisateurs ont interagi avec le chatbot, donnant lieu à 5'969 conversations et <strong>11'781 questions</strong> (soit environ deux questions par conversation). </p>
<p>Les feedbacks donnés directement au chatbot sont à <strong>75% positifs</strong>. </p>
<h2>Une bonne acceptabilité de ce chatbot différent des autres</h2>
<p>Si les chatbots dans le parcours de santé sont bien acceptés par les patients par leur disponibilité continue et leur facilité d’usage, des études montrent que les problèmes décelés concernent l’hétérogénéité de la qualité des réponses données et l’opacité des sources utilisées. </p>
<p>Ce sont là des éléments qui permettent de distinguer ConfIAnce d’autres chatbots médicaux. </p>
<p>Destiné à <strong>encourager, et non à remplacer, les relations directes entre les patients et les médecins</strong>, ConfIAnce “permet de soutenir les médecins de premier recours en leur libérant du temps de qualité pour pratiquer avec humanité le métier qu’ils ont choisi d’exercer.”</p>
<p>Les auteur·rices de l’article de la Revue Médicale Suisse soulignent à quel point ce chatbot développé dans le contexte spécifique des HUG et de leurs bases d’information peut être transposé dans d’autres contextes institutionnels. Pour que la réussite soit au rendez-vous, il nous semble nécessaire de bénéficier d'abord de <strong>données de qualité</strong> comme c’était le cas ici. Mais ce n’est pas tout, <strong>les couches de contrôle, les tests automatiques et les feedbacks utilisateurs</strong> permettent ensuite une amélioration continue pour garantir la sécurité et la pertinence nécessaires à la confiance.</p>]]></description>
    </item>
        <item>
      <title>Rendre LiipGPT accessible: notre parcours vers la conformit&#233; WCAG-AA</title>
      <link>https://www.liip.ch/fr/blog/rendre-liipgpt-accessible</link>
      <guid>https://www.liip.ch/fr/blog/rendre-liipgpt-accessible</guid>
      <pubDate>Mon, 16 Mar 2026 00:00:00 +0100</pubDate>
      <description><![CDATA[<p>Après nous être d’abord concentré·e·s sur l’approche thématique de notre chatbot <a href="https://www.liipgpt.ch/" rel="noreferrer" target="_blank">LiipGPT</a> — présentée récemment lors du <a href="https://zuericitygpt.ch/" rel="noreferrer" target="_blank">relaunch de Z&uuml;riCityGPT</a> — nous avons orienté notre attention vers l’accessibilité avec pour objectif d’atteindre la conformité WCAG-AA. Comme pour beaucoup de fonctionnalités, nous avons d’abord observé comment les référence du secteur comme ChatGPT, Perplexity et Claude traitent l’accessibilité. Bien que nous ayons constaté un potentiel d’amélioration partout, cela nous a inspiré·e·s à réfléchir à la manière de faire mieux de notre côté.</p>
<p>Notre parcours vers l’accessibilité s’est articulé autour de quatre étapes principales: des scans automatiques et quick fixes, la navigation au clavier, l’optimisation pour le zoom mobile ainsi que l’optimisation pour les lecteur·rice·s d’écran.</p>
<h2>Scans automatiques et quick fixes</h2>
<p>Nous avons commencé par des tests automatisés d’accessibilité à l’aide d’extensions de navigateur comme <a href="https://chromewebstore.google.com/detail/ibm-equal-access-accessib/lkcagbfjnkomcinoddgooolagloogehp" rel="noreferrer" target="_blank">IBM Equal Access Accessibility Checker</a> et <a href="https://www.deque.com/axe/devtools/extension" rel="noreferrer" target="_blank">axe DevTools</a>. Ces outils nous ont aidé·e·s à identifier des problèmes fréquents: labels manquants, contraste de couleurs insuffisant, HTML sémantique incorrect et attributs ARIA absents. Bien que les scans automatisés ne détectent qu’environ 40 % des problèmes d’accessibilité, ils constituent une base  de départ solide.</p>
<h2>Navigation au clavier</h2>
<p>Une navigation correcte au clavier est fondamentale pour l’accessibilité. Garantir que la navigation de base avec la touche Tab fonctionne dans toute l’application est relativement simple. Des composants plus complexes comme les <a href="https://www.w3.org/WAI/ARIA/apg/patterns/tabs/examples/tabs-automatic/" rel="noreferrer" target="_blank">tabs</a>, les <a href="https://www.w3.org/WAI/ARIA/apg/patterns/menubar/" rel="noreferrer" target="_blank">menus</a> et les <a href="https://www.w3.org/WAI/ARIA/apg/patterns/dialog-modal/" rel="noreferrer" target="_blank">fen&ecirc;tres de dialogue</a> nécessitent cependant des interactions clavier plus avancées comme l'utilisation des flèches directionnelles, la gestion de la touche Escape ou encore la gestion du focus selon les directives officielles du W3C. Les utilisateur·rice·s qui dépendent de la navigation au clavier s’attendent à ces schémas spécifiques. S’en écarter provoque confusion et frustration. Plutôt que d’implémenter ces schémas depuis zéro, nous avons utilisé <a href="https://bits-ui.com/" rel="noreferrer" target="_blank">Bits UI</a>, une bibliothèque headless UI qui applique correctement ces directives d’accessibilité.</p>
<p>Au-delà des composants individuels, nous avons également implémenté des boucles de focus et une restauration du focus au niveau de l’application afin que les utilisateur·rice·s restent toujours orienté·e·s en passant d’une zone de l’interface du chat à une autre.</p>
<h2>Optimisation pour le zoom mobile</h2>
<p>Lors de tests utilisateur pour <a href="https://meinplatz.ch/" rel="noreferrer" target="_blank">meinplatz.ch</a> avec des personnes en situation de handicap, nous avons observé quelque chose de frappant: beaucoup naviguent sur les sites web sur mobile avec un zoom de 200 % ou plus et tiennent leur appareil à environ 10 cm de leurs yeux. Cette observation a mis en évidence une lacune critique dans la plupart des implémentations de chatbots.</p>
<p>La majorité des chatbots utilisent des éléments à position fixe: un champ de saisie en bas de l’écran et souvent un en-tête en haut. Lorsque les utilisateur·rice·s zooment fortement, ces éléments fixes peuvent occuper tout le viewport et rendre l’interface inutilisable. Malheureusement, les navigateurs ne permettent pas de détecter de manière fiable le niveau de zoom. Pour résoudre cela, nous utilisons l’Intersection Observer pour détecter lorsque l’en-tête ou le pied de page occupe plus d’espace que prévu. Dans ce cas, nous supprimons dynamiquement leur position fixe afin de rétablir l’utilisabilité.</p>
<figure class="video"><video autoplay controls loop muted playsinline><source src="https://www.liip.ch/media/pages/blog/making-liipgpt-accessible/343fac4583-1769071651/chatgpt-zoom.mp4" type="video/mp4"></video><figcaption>Des &eacute;l&eacute;ments &agrave; position fixe causent des probl&egrave;mes dans les vues fortement zoom&eacute;es.</figcaption></figure>
<figure class="video"><video autoplay controls loop muted playsinline><source src="https://www.liip.ch/media/pages/blog/making-liipgpt-accessible/6900992c5e-1769071651/liipgpt-zoom.mp4" type="video/mp4"></video><figcaption>La solution: r&eacute;tablir une position statique pour les &eacute;l&eacute;ments fixes lorsqu&rsquo;un zoom est d&eacute;tect&eacute;.</figcaption></figure>
<h2>Expérience avec les lecteurs d’écran</h2>
<p>L’accessibilité pour les lecteur·rice·s d’écran n’apparaît pas automatiquement, elle nécessite une conception attentive. Nous nous sommes concentré·e·s sur la fourniture d’un contexte clair grâce à une structure de page propre (landmarks et titres). Les utilisateur·rice·s comprennent ainsi toujours où iels se trouvent et ce qui se passe, tout en disposant de raccourcis vers les zones les plus importantes de l’application.</p>
<h4>Fournir du contexte</h4>
<p>Nous avons implémenté une structure d’outline complète avec des landmarks pour la navigation principale, les paramètres et les zones de saisie. Chaque message contient des titres et des labels corrects. De plus, nous avons ajouté après le champ de saisie du chat (en bas de page) un skip-link permettant aux utilisateur·rice·s de revenir rapidement en haut de la page.</p>
<h4>Défis avec les Web Components</h4>
<p>Travailler avec des Web Components a apporté ses propres défis. VoiceOver est particulièrement sensible à la manière dont les bibliothèques sont implémentées. Nous avons travaillé en étroite collaboration avec l’équipe Bits UI (qui réagit très rapidement aux rapports de bugs) et avons par exemple implémenté des portails locaux pour les menus déroulants afin d’éviter des problèmes de navigation avec VoiceOver.</p>
<h4>Gestion des annonces</h4>
<p>L’un des défis les plus complexes a été la gestion des annonces VoiceOver lorsque plusieurs événements se produisent simultanément. Comme la mise en file des annonces ne fonctionne pas de manière fiable, nous avons soigneusement séquencé les événements et regroupé les annonces liées. Par exemple, lorsqu’un·e utilisateur·rice clique sur « Tout sélectionner » dans une liste d’options, des annonces séparées seraient normalement déclenchées pour chaque option et se remplaceraient mutuellement. À la place, nous annulons ces annonces individuelles et les remplaçons par un seul message clair qui résume la situation (tous les éléments sélectionnés ou désélectionnés, retour à la sélection prédéfinie, etc.).</p>
<p>Comme le chat est une SPA sans rechargement de page, il était également essentiel d’annoncer tous les changements visibles uniquement visuellement, comme par exemple le changement entre mode clair et sombre ou le changement de langue.</p>
<h4>Flux du chat pour les lecteur·rice·s d’écran</h4>
<p>Nous avons conçu l’expérience du chat spécifiquement pour les utilisateur·rice·s de lecteur d’écran :</p>
<ul>
<li>Le champ de saisie contient à la fois un placeholder et un aria-label avec le titre de la page. Cela fournit du contexte lors du chargement de la page, car le champ est automatiquement focalisé et les utilisateur·rice·s peuvent ainsi ignorer le contenu initial.</li>
<li>Lorsqu’une réponse est générée, nous l’annonçons clairement afin de fournir le même retour qu’un indicateur de chargement visuel.</li>
<li>Dès qu’une réponse est prête, elle est lue sans formatage Markdown (pas d’italique, pas de liens, etc.) afin de garantir un flux de lecture naturel.</li>
<li>Après la lecture d’une réponse, nous indiquons que tu peux directement poser une nouvelle question ou naviguer vers les options du dernier message pour donner un feedback ou consulter les références. Nous ajoutons dynamiquement cette section interactive du dernier message à l’outline du document afin de créer un raccourci de navigation rapide.</li>
<li>L’historique du chat est structuré comme une série d’articles avec des labels afin que les conversations précédentes soient faciles à parcourir.</li>
</ul>
<figure class="video"><video autoplay controls loop muted playsinline><source src="https://www.liip.ch/media/pages/blog/making-liipgpt-accessible/912fd0d7cd-1769018708/screenreader.mp4" type="video/mp4"></video><figcaption>La solution: navigation dans le chatbot avec le lecteur d&rsquo;&eacute;cran VoiceOver sur macOS.</figcaption></figure>
<h2>Teste par toi-même</h2>
<p>Tu peux découvrir ces améliorations avec <a href="https://www.bs.ch/alva" rel="noreferrer" target="_blank">Alva</a>, le chatbot de l’administration du canton de Bâle-Ville. Essaie de naviguer avec <a href="https://www.google.com/search?q=how+to+navigate+a+website+with+voiceover" rel="noreferrer" target="_blank">VoiceOver (macOS)</a> ou <a href="https://www.google.com/search?q=how+to+navigate+a+website+with+nva+screen+reader" rel="noreferrer" target="_blank">NVA (Windows)</a>, d’utiliser uniquement ton clavier ou de zoomer fortement sur un appareil mobile.</p>
<h2>Un parcours continu</h2>
<p>Notre prochain objectif est d’intégrer des tests automatisés d’accessibilité dans notre pipeline CI. Comme mentionné précédemment, les scans automatisés ne détectent qu’environ 40 % des problèmes d’accessibilité. Cela signifie que nous devons continuer à planifier soigneusement chaque nouvelle fonctionnalité et à la tester manuellement. Rien ne remplace les tests humains lorsqu’il s’agit d’accessibilité. Les outils automatisés peuvent signaler des labels manquants ou des problèmes de contraste, mais ils ne peuvent pas déterminer si une interface est réellement utilisable pour quelqu’un qui navigue avec un lecteur d’écran ou uniquement au clavier.</p>
<p>L’accessibilité est un parcours continu, pas une destination. Nous nous engageons à rendre LiipGPT utilisable par tout le monde et continuerons à faire évoluer notre approche sur la base des retours issus de la pratique.</p>
<h2>Besoin d’aide pour l’accessibilité ?</h2>
<p>Nous proposons des audits d’accessibilité pour identifier et corriger les problèmes dans tes applications. Si tu souhaites améliorer l’accessibilité de ton produit, <a href="https://www.liip.ch/en/contact">contacte-nous</a>, nous serons ravi·e·s de t’aider.</p>]]></description>
    </item>
        <item>
      <title>Perspectives sur l&#8217;IA et l&#8217;open source pour les administrations publiques &#224; Drupal4Gov</title>
      <link>https://www.liip.ch/fr/blog/perspectives-sur-l-ia-et-l-open-source-pour-les-administrations-publiques-a-drupal4gov</link>
      <guid>https://www.liip.ch/fr/blog/perspectives-sur-l-ia-et-l-open-source-pour-les-administrations-publiques-a-drupal4gov</guid>
      <pubDate>Wed, 11 Mar 2026 00:00:00 +0100</pubDate>
      <description><![CDATA[<p>La conférence Drupal4Gov était riche en présentations passionnantes dont tu trouveras ici mes moments favoris. J’y étais aussi pour présenter notre travail sur le projet Kanton Basel-Stadt / Alva / blökkli. Nous en avons déjà parlé, mais l’article est l’occasion de faire le point sur le projet et ses nouvelles fonctionnalités.</p>
<h2>GovNL: de plusieurs mois à quelques minutes pour créer des sites</h2>
<p>GovNL associe des composants Drupal open source et un design system ouvert pour faire fonctionner de nombreux sites du gouvernement néerlandais de manière accessible et évolutive. Le temps nécessaire pour créer un nouveau site passe alors <strong>d’environ trois mois à une dizaine de minutes</strong> seulement! Plutôt impressionnant, n’est-ce pas? C’est un excellent exemple de conception pensée pour la réutilisation à grande échelle.</p>
<h2>Commission européenne: la coordination est essentielle pour passer à l’échelle</h2>
<p>La Commission européenne gère déjà pas moins de <strong>770 sites</strong> et investit fortement dans l’écosystème Drupal. Ce qui m’a marqué, c’est l’importance qu’elle accorde à <strong>la coordination</strong>, soit s’assurer que le bon contenu est publié sur le bon canal à travers cet ensemble de sites. Les Open Source Program Offices (OSPO) ont été mis en place pour piloter les stratégies open source tant au niveau gouvernemental qu'au sein des organisations.</p>
<figure><img alt="" src="https://liip.rokka.io/www_inarticle_5/ac3271/drupal4gov2026-josef.jpg" srcset="https://liip.rokka.io/www_inarticle_5/o-dpr-2/ac3271/drupal4gov2026-josef.jpg 2x"></figure>
<h2>Site web du canton de Bâle-Ville et Alva: modèle pour les administrations publiques locales</h2>
<p>Juste avant midi, c’était à mon tour de présenter les <strong>différentes applications de l'IA</strong> que nous avons mis en place pour le <a href="https://www.liip.ch/fr/work/projects/basel-stadt">canton de B&acirc;le-Ville</a>. Avec la nouvelle version de bs.ch, le canton a établi de nouveaux standards avec un design centré sur les utilisateur·rices, un accès aux contenus thématiques plutôt que basés sur la structure administrative interne et Alva, le <strong>premier chatbot basé sur l’IA pour un canton</strong> suisse. La stack repose sur des composants open source et Liip a fortement contribué à l’open source dans le cadre de ce projet. Nous utilisons Drupal comme CMS, Nuxt/Vue, l’éditeur <a href="https://blokk.li/">bl&ouml;kkli</a> pour le frontend headless et Elasticsearch pour la recherche. Le contenu est produit par une équipe éditoriale transversale entre départements, suivant une stratégie de contenu claire.</p>
<h2>L’IA pour soutenir le public et les éditeur·rices du site</h2>
<p>Cette présentation était aussi l’occasion de partager des chiffres plus de 18 mois après la mise en ligne. Aujourd’hui, Alva traite <strong>plus de 10 000 questions par mois</strong>, avec environ <strong>1,36 question par conversation et une croissance de +44 % depuis Alva 2.0</strong>. Grâce aux intégrations API, le chatbot peut répondre à des questions en s’appuyant sur des informations en temps réel. Alva est également très utilisé par les collaborateur·rices internes du canton, en plus du grand public. Le chatbot affiche et vérifie toujours ses sources, un élément central pour instaurer la confiance. </p>
<p>Du côté de l’édition de contenu, blökkli travaille activement à simplifier les textes. Grâce à <strong>l’éditeur blökkli</strong> avec IA intégrée, les éditeur·rices peuvent désormais effectuer un audit de lisibilité, voir des propositions de simplification côte à côte et les accepter ou les adapter. Alva et les fonctionnalités d’IA sur bs.ch continuent d’être développées afin d’offrir aux éditeur·rices et aux citoyen·nes des technologies d’IA fiables.</p>
<h2>Des technologies IA au sein du gouvernement français</h2>
<p>Une autre présentation inspirante portait sur les cas d’usage de l’IA dans la plateforme Services Publics+ du gouvernement français. Avec plus de 140 000 expériences partagées et plus d’un million de réactions, le système utilise des technologies d’IA assistée pour aider les services de l’État à fournir des retours aux citoyen·nes. Ils utilisent notamment la transcription automatique de la parole (speech-to-text) et des résumés en temps réel comme technologies clés. Sacrés français:) </p>
<h2>L’UE fait plus que jamais confiance à l’open source</h2>
<p>L’Union européenne a introduit <strong>Website Evidence Collector</strong>, un outil open source qui analyse les sites web pour détecter des problèmes de sécurité. Il est publié sous licence <strong>EUPL</strong> (European Union Public Licence), ce qui met l’accent sur l’interopérabilité entre pays et licences et favorise la collaboration multilingue. Je me demande si la Suisse dispose d’un outil similaire.</p>
<p>Mais l’UE ne se contente pas de faire confiance à l’open source pour la sécurité. Avec Interoperable Europe, elle propose également un nouveau portail comprenant un <strong>Licensing Assistant</strong> très utile. Tu peux y <a href="https://interoperable-europe.ec.europa.eu/collection/eupl/solution/licensing-assistant/find-and-compare-software-licenses">trouver et comparer des licences logicielles</a> et utiliser un <a href="https://interoperable-europe.ec.europa.eu/collection/eupl/solution/licensing-assistant/compatibility-checker">outil de v&eacute;rification de compatibilit&eacute;</a> pour voir si différentes licences open source peuvent être combinées et s’il existe des complications juridiques.</p>
<h2>Utiliser l’open source ne suffit pas, il faut des champion·nes</h2>
<p>Enfin, <strong>Tiffany Farris</strong>, du cabinet de conseil stratégique <a href="https://www.palantir.net/">Palantir.net</a> (à ne pas confondre avec la controversée Palantir Technologies), a rappelé que <strong>l’utilisation de l’open source est une bonne chose mais n’est pas suffisante</strong>. Il faut <strong>des champion·nes</strong> dans les organisations qui mettent la contribution et la santé de l’écosystème à l’agenda. Concevoir pour la réutilisation devrait être un principe fondamental. Du point de vue américain, la commande publique reste un problème: l’usage de l’open source a augmenté, mais les mécanismes de soutien n’ont souvent pas suivi. Considérer l’open source comme « gratuit » peut conduire à attribuer des contrats à des fournisseurs qui présentent leur travail comme open source sans réellement soutenir un écosystème dynamique. Elle a enfin proposé des modifications concrètes des politiques de marchés publics dans l’esprit <strong>« public money, public code »</strong>, afin de mieux soutenir l’écosystème. Une conclusion vraiment inspirante pour une journée riche en apprentissages et en échanges.</p>
<p>Tu peux regarder <a href="https://www.youtube.com/playlist?list=PLNubpNMwP36QH5Y3RlbOiV4f9hjlrxCOo">la playlist</a> des présentations Drupal4Gov EU 2026 si tu souhaites approfondir les sujets abordés.</p>]]></description>
    </item>
        <item>
      <title>Web Components: The Good, the Bad, and the Ugly</title>
      <link>https://www.liip.ch/fr/blog/web-components-the-good-the-bad-and-the-ugly</link>
      <guid>https://www.liip.ch/fr/blog/web-components-the-good-the-bad-and-the-ugly</guid>
      <pubDate>Wed, 11 Mar 2026 00:00:00 +0100</pubDate>
      <description><![CDATA[<h1>Introduction</h1>
<p>We created a fully themeable chat UI that can be embedded in any website and has no effect on the parent page. <a href="https://www.bs.ch/alva">Kanton Basel-Stadts Alva</a> and <a href="https://ramms.ch/">RAMMS' Rocky AI</a> are instances of that UI.</p>
<p>This blog post will show you what we learned about creating web components that do not influence the parent page. Here are the good, the bad and the ugly when working with web components.</p>
<h1>The Good</h1>
<p>These are the good parts of web components. They will lay the foundation for why you might use them.</p>
<h2>Portability</h2>
<p>Every system that can handle HTML can handle web components. A simple tag and a script will integrate it into any web framework. It doesn't even need to be a JavaScript framework.</p>
<pre><code class="language-html">&lt;body&gt;
  &lt;your-webcomponent&gt;&lt;/your-webcomponent&gt;
  &lt;script src="path/to/your-webcomponent.js"&gt;&lt;/script&gt;
&lt;/body&gt;</code></pre>
<h2>Native Feel</h2>
<p>IFrames are another way to embed UI into a page, and they are arguably easier to use. But the main difference is that web components feel more native to the page, since they directly integrate into the parent page's layout. This means you can use transparency, intrinsic sizing (size based on the web component's contents), and seamless event communication with the parent page.</p>
<h2>Slots</h2>
<p>With slots you can provide content that will be added at a specified point inside your web component.</p>
<p>In our chat UI, we used a slot to let integrators provide a custom loading spinner. This spinner needs to be visible immediately, before the full theme loads asynchronously.</p>
<h2>Shadow DOM - Isolating Styles</h2>
<p>A robust way to ensure that your styles do not affect the parent page is to use the Shadow DOM. Shadow DOM is a web component feature to add a boundary for styles. Styles applied inside the Shadow DOM never apply to the parent page.</p>
<h3>Caveat: Inheritable CSS Properties</h3>
<p>There is one exception to the isolation where CSS properties of the parent page apply to the web component.</p>
<p>These are the properties that pierce through the boundary:</p>
<ul>
<li>Inheritable CSS properties like <code>color</code>, <code>font-family</code>, <code>line-height</code></li>
<li>CSS custom properties like <code>--my-var</code></li>
</ul>
<p>In practice, we have found it helps to fully specify the common properties like fonts and color on every element. That way you will never be surprised by different styles on integration.</p>
<h1>Vite</h1>
<p>For bundling web components, we can highly recommend Vite. There are a lot of neat tricks you can apply while bundling. Here are the Vite features we used for our web component.</p>
<h2>Inlining Assets</h2>
<p>Vite's <a href="https://vite.dev/guide/assets#explicit-inline-handling">explicit inline handling</a> feature allowed us to inline our external CSS files into the JS bundle.</p>
<pre><code class="language-ts">import cssContentString from "./index.css?inline";</code></pre>
<p>This feature will not only inline the raw content of the imported <code>index.css</code>. It will also resolve all CSS imports, apply PostCSS transforms, and even work with CSS preprocessors like SASS. While inlined CSS is not the most efficient for browsers to render, the benefit is that we can ship a single JS file.</p>
<h2>Library Mode</h2>
<p>The Vite <a href="https://vite.dev/guide/build#library-mode">library mode</a> provides you with fine-grained control of how the bundle should behave. To enable the library mode just add the <code>build.lib</code> option in your Vite config.</p>
<h1>The Bad</h1>
<p>Not everything about web components is great though. Here are the bad parts.</p>
<h2>SSR - Hard to Get Working</h2>
<p>Server-side rendering will almost certainly not work. The rest of the page can still be rendered server-side, but the web component will only show up as a <code>&lt;your-webcomponent&gt;&lt;/your-webcomponent&gt;</code> tag. Its contents will only be rendered in the browser.</p>
<p>There is one <a href="https://lit.dev/docs/ssr/overview/">experimental package by Lit Labs</a> that tries to solve this, but we never tried it.</p>
<h2>Tailwind - Not a Great Fit</h2>
<p>Tailwind feels like a natural choice for web components, but it does not play well with them.</p>
<p>The core issue is twofold. First, Tailwind ships its own CSS reset (called Preflight), which overrides default browser styles. When injected into a page that does not use Tailwind, it potentially breaks the page. Shadow DOM could isolate this reset, but Tailwind is fundamentally not designed to work inside a Shadow DOM. Here is the <a href="https://github.com/tailwindlabs/tailwindcss/discussions/1935">discussion</a> if you are interested.</p>
<p>There are some hacky workarounds, but we tried them and had no success getting them to work reliably.</p>
<p>Our recommendation is to only use Tailwind if you are guaranteed that the parent page also uses it, and then use web components without Shadow DOM.</p>
<h1>The Ugly</h1>
<h2>Verbose Web Components API</h2>
<p>The native web component API is verbose and hard to read. A simple counter component, for example, requires manually defining a class, attaching a shadow root, setting up <code>innerHTML</code>, and wiring event listeners in <code>connectedCallback</code>. This boilerplate adds up quickly. You can see examples of the API <a href="https://github.com/mdn/web-components-examples">here</a>.</p>
<p>Fortunately, web components make for a great compile target. <a href="https://svelte.dev/docs/svelte/custom-elements">Svelte</a> and <a href="https://vuejs.org/api/custom-elements.html#definecustomelement">Vue</a> directly support compiling to web components. <a href="https://blog.logrocket.com/working-custom-elements-react/">React</a> is a bit trickier, but totally doable as well. We used this approach for our chat UI, where the first iteration was built with React and the current one with Svelte.</p>
<h1>Weird Quirks</h1>
<p>Advanced web component features come with edge cases that no documentation warns you about. Even Svelte, which has excellent web component support, ships with a notable <a href="https://svelte.dev/docs/svelte/custom-elements#Caveats-and-limitations">list of caveats</a>.</p>
<p>We even hit an undocumented edge case with slots in Svelte: the bundle script must load after the component markup, or slotted content will not render. An ugly <a href="https://github.com/FalkZ/svelte-web-components-starter/blob/main/src/slot.svelte">wrapper for slots</a> fixes the problem, but quirks like this add up and slow you down.</p>
<h2>Font Loading - Not Working Inside Shadow DOM</h2>
<p>When authoring web components, you get into the habit of defining all stylesheet links etc. in the web component body. As you should, so they do not affect the parent page. But there is another annoying detail here: @font-face will not work when defined in the Shadow DOM. If your web component needs a custom font, you need to inject the font CSS into the parent page to make it work.</p>
<h1>Conclusion</h1>
<p>I do not want to end on this ugly note though. I really think there are cases where web components are the right choice, and in our case we would choose Svelte &amp; web components again.</p>
<p>To help you get started, here is a <a href="https://github.com/FalkZ/svelte-web-components-starter">Svelte starter template</a>.</p>]]></description>
    </item>
        <item>
      <title>City of Zurich&#039;s 900+ Open Data Sets Now Have an MCP Server</title>
      <link>https://www.liip.ch/fr/blog/city-of-zurich-s-900-open-data-sets-now-have-an-mcp-server</link>
      <guid>https://www.liip.ch/fr/blog/city-of-zurich-s-900-open-data-sets-now-have-an-mcp-server</guid>
      <pubDate>Thu, 26 Feb 2026 00:00:00 +0100</pubDate>
      <description><![CDATA[<p><a href="https://www.linkedin.com/in/alexander-g%C3%BCntert-3379071b6/">Alexander Güntert</a> <a href="https://www.linkedin.com/posts/activity-7432101739589345280-0YcB">posted on LinkedIn</a> about a new open-source project his colleague <a href="https://www.linkedin.com/in/hayaloezkan/">Hayal Oezkan</a> had built: an <a href="https://github.com/malkreide/zurich-opendata-mcp">MCP server for Zurich's open data</a>. The post got quite some reactions and I liked the idea very much. But it still needed a local installation, not something non-developers easily know how to do. So I had it packaged and deployed on our servers, available for anyone to use as the "OGD City of Zurich" remote MCP server.</p>
<p>The City of Zurich publishes over 900 datasets as open data, spread across six different APIs. There's <a href="https://data.stadt-zuerich.ch">CKAN</a> for the main data catalog, a WFS Geoportal for geodata, the Paris API for parliamentary information from the Gemeinderat, a tourism API, SPARQL linked data, and ParkenDD for real-time parking data. All public, all freely available. But until now, making an AI assistant actually use these APIs meant writing custom integrations for each one.</p>
<p>The MCP server wraps all six APIs into 20 tools that any MCP-compatible AI assistant can call directly. Ask "How warm is it in Zurich right now?" and it queries the live weather stations. Ask about parking availability, and it pulls real-time data from 36 parking garages. It also covers parliamentary motions, tourism recommendations, SQL queries on the data store, and GeoJSON features for school locations, playgrounds, or climate data. All through a single, standardized <a href="https://modelcontextprotocol.io/">Model Context Protocol</a> interface.</p>
<p>Hayal Oezkan  built it in Python using FastMCP. One file for the server with all 20 tool handlers. The <a href="https://github.com/malkreide/zurich-opendata-mcp">repo</a> is on GitHub.</p>
<p>Deploying it on our side took very little effort. The server supports both stdio transport for local use (like in Claude Desktop or Claude Code) and SSE and HTTP Streaming for remote deployment. I packaged it with Docker, deployed it to our cluster, and now it's available as a remote MCP server that anyone can add to their AI tools without installing anything locally.</p>
<p>The natural next step was integrating this with <a href="https://zuericitygpt.ch/">ZüriCityGPT</a>. It happened, just not quite in the direction I originally had in mind.</p>
<p>ZüriCityGPT already had its own MCP server at zuericitygpt.ch/mcp, exposing tools for searching the city's website content, "Stadtratsbeschlüsse" and looking up waste collection schedules. Instead of wiring the open data tools into ZüriCityGPT, I went the other way: the open data MCP server now proxies tools from the ZüriCityGPT MCP server. A lightweight proxy client connects to the remote server via streamable-http and forwards calls. The whole thing is about 40 lines of Python.</p>
<p>So now, when you connect to the Zurich Open Data MCP server, you get 23 tools in one place. The 21 original open data tools across six APIs, plus <code>zurich_search</code> for querying the city's knowledge base and <code>zurich_waste_collection</code>  for waste pickup schedules (based on the <a href="https://openerz.metaodi.ch/documentation">OpenERZ API</a>). One MCP endpoint, many services behind it. </p>
<p>A city employee builds something useful in the open, publishes the code, and within a day it's deployed and available to a wider audience. Open data and open source working together, exactly as intended.</p>]]></description>
    </item>
        <item>
      <title>Le canton des Grisons lance son nouveau site web</title>
      <link>https://www.liip.ch/fr/blog/le-canton-des-grisons-lance-son-nouveau-site-web</link>
      <guid>https://www.liip.ch/fr/blog/le-canton-des-grisons-lance-son-nouveau-site-web</guid>
      <pubDate>Wed, 25 Feb 2026 00:00:00 +0100</pubDate>
      <description><![CDATA[<p>Le canton des Grisons développe un nouveau site web. La structure actuelle est basée sur l’organisation administrative et comprend <strong>plusieurs dizaines de milliers de pages</strong>. Le site existant ne répond plus aux exigences actuelles en matière d’utilisabilité ni à l’évolution des comportements des utilisateur·rice·s.</p>
<p>Au cœur de la refonte se trouve l'orientation utilisateur·rice. Au lieu d’une perspective interne, dictée par l’administration, une structure orientée par thématiques est mise en place — à l’image de ce que le <a href="https://www.liip.ch/fr/work/projects/basel-stadt" rel="noreferrer" target="_blank">canton de B&acirc;le-Ville a d&eacute;j&agrave; r&eacute;alis&eacute; avec succ&egrave;s.</a></p>
<p><strong>D’un point de vue contenu, le projet est gigantesque.</strong></p>
<h1>De quoi un projet de contenu de cette ampleur a-t-il besoin?</h1>
<p>Un contenu développé de manière organique ne peut pas être simplement migré. Une refonte est généralement le moment où le contenu est non seulement transféré, mais aussi repensé en profondeur, notamment en termes de qualité.</p>
<p>Comme un volume très important de contenus doit être retravaillé, un <strong>modèle éditorial décentralisé</strong> est indispensable. De nombreux·ses collaborateur·rice·s — souvent sans grande expérience en matière de contenu — sont impliqué·e·s. <strong>De bons processus, des rôles clairs et un accompagnement ciblé</strong> sont donc essentiels:</p>
<ul>
<li>Une direction de projet décisionnelle</li>
<li>Un inventaire et une évaluation solides</li>
<li>Un concept et une stratégie clairs</li>
<li>Une gouvernance (processus &amp; responsabilités)</li>
<li>La mise en place et la formation de la rédaction web</li>
<li>La production opérationnelle des contenus</li>
<li>L’assurance qualité</li>
</ul>
<p>Nous accompagnons le canton des Grisons dans cette démarche. Sur la base de notre expérience acquise lors de projets menés à Bâle-Ville, Soleure et dans différents offices fédéraux, une collaboration efficace et fondée sur la confiance s’est développée.</p>
<h1>Comment nous collaborons</h1>
<p>Deux <a href="https://www.liip.ch/fr/services/content/strategic-storytelling" rel="noreferrer" target="_blank">de nos strat&egrave;ges de contenu</a> travaillent en étroite collaboration avec la direction de projet du canton. L’équipe de projet apporte ses connaissances internes et son pouvoir décisionnel, tandis que Liip contribue avec sa capacité opérationnelle et sa vaste expérience issue de configurations similaires. Cette organisation permet une grande rapidité — et une intégration fluide des savoirs internes et des apports externes.</p>
<p>La collaboration dure déjà depuis plus d'un an et demi.</p>
<h2>Quelles sont nos prestations</h2>
<ul>
<li>Audit de contenu</li>
<li>Stratégie de contenu &amp; tonalité</li>
<li>Gouvernance de contenu &amp; modèle éditorial (pour la phase projet et pour l’exploitation future)</li>
<li>Modèle de cycle de vie des contenus</li>
<li>Directives de contenu pour les équipes éditoriales</li>
<li>Modèles de pages et exemples de textes (en collaboration avec l'équipe design)</li>
<li>Modèles Word pour la création de contenu</li>
<li>Formations et accompagnement des membres de la rédaction</li>
<li>Outil de rédaction basé sur l’IA « GR Editor » (basé sur <a href="https://liip-textmate.liipgpt.ch/" rel="noreferrer" target="_blank">TextMate</a>)</li>
<li>Assurance qualité</li>
</ul>
<figure><img alt="" src="https://liip.rokka.io/www_inarticle_5/adf31c/gr-work-layers-01-fr.jpg" srcset="https://liip.rokka.io/www_inarticle_5/o-dpr-2/adf31c/gr-work-layers-01-fr.jpg 2x"></figure>
<h1>Concrètement: de l’audit à l’assurance qualité</h1>
<p>Une refonte impliquant plus de 120 éditeur·rice·s et des milliers de pages de contenu nécessite avant tout une chose: un plan de mise en œuvre clair et une préparation minutieuse. Chaque étape s’appuie sur la précédente.</p>
<ol>
<li><a href="https://www.liip.ch/fr/services/content/content-audit" rel="noreferrer" target="_blank">L&#039;audit de contenu audit</a> pour créer de la clarté</li>
</ol>
<p>Nous avons commencé le projet par l’analyse d’un échantillon représentatif des contenus existants. L’audit a permis d’identifier les potentiels d’optimisation et a servi de base à toutes les décisions ultérieures.</p>
<ol start="2">
<li>Le modèle éditorial: rôles, responsabilités et processus</li>
</ol>
<p>La <a href="https://www.liip.ch/fr/services/content/content-governance" rel="noreferrer" target="_blank">gouvernance de contenu</a> et le modèle éditorial développés ont posé les bases du travail avec les équipes de rédaction web. Un modèle a été conçu pour la phase projet, complété par une planification pour l’après-lancement.</p>
<ol start="3">
<li>Stratégie de contenu et tonalité comme cadre directeur</li>
</ol>
<p>La gouvernance régit la collaboration et les processus ; la stratégie de contenu définit l’orientation éditoriale. Elle s’adresse moins aux éditeur·rice·s qu’à la définition du cadre conceptuel et rédactionnel pour les éléments opérationnels. La stratégie a été complétée par deux tonalités distinctes pour le nouveau site web.</p>
<p>Une fois la stratégie finalisée, la phase opérationnelle a débuté.</p>
<ol start="4">
<li>Modèles de pages et exemples de textes pour rendre le concept tangible</li>
</ol>
<p>En collaboration avec l’équipe design, nous avons développé les nouveaux types de pages. Notre <a href="https://www.liip.ch/fr/services/content/ux-writing" rel="noreferrer" target="_blank">UX writer</a> a créé des contenus réalistes servant de référence pour la finalisation des types de pages.</p>
<p><strong>Travailler avec de vrais textes est crucial</strong> car c’est la seule manière d’aligner efficacement structure, contenu et design.</p>
<ol start="5">
<li><a href="https://www.liip.ch/fr/services/content/guidelines" rel="noreferrer" target="_blank">Les directives de contenu</a> comme manuel pour les rédactions</li>
</ol>
<p>Dérivées de la stratégie de contenu, les directives regroupent règles, recommandations, exemples et instructions concrètes. Elles facilitent des contenus clairs, accessibles et cohérents.</p>
<ol start="6">
<li>Des modèles Word car le CMS est développé en parallèle</li>
</ol>
<p>Comme le développement du CMS et la création de contenu se déroulent <strong>en parallèle</strong>, les textes doivent être rédigés en dehors du CMS. À cet effet, nous avons créé des modèles Word qui :</p>
<ul>
<li>reflètent la nouvelle structure des pages,</li>
<li>intègrent toutes les métadonnées pertinentes et</li>
<li>fournissent des indications aux éditeur·rice·s.</li>
</ul>
<p>Les modèles Word ont été complétés par <strong>des visualisations des différents types de pages</strong>. Pour chaque type de page, un exemple de texte réel a été conçu dans Figma et partagé avec les équipes éditoriales en PDF.</p>
<ol start="7">
<li>Outil de rédaction IA « GR Editor » – un soutien en temps réel</li>
</ol>
<p>Un modèle éditorial décentralisé entraîne inévitablement des différences de qualité rédactionnelle. Le GR Editor (basé sur <a href="https://www.liip.ch/fr/blog/textmate" target="blank">TextMate</a>) permet de contrebalancer cela en vérifiant la clarté, la cohérence et la tonalité directement pendant la rédaction.</p>
<p>Le canton disposait de conditions idéales : des directives rédactionnelles cantonales existantes, les nouvelles directives de contenu et deux tonalités clairement définies.</p>
<ol start="8">
<li>Mise en place des équipes éditoriales et <a href="https://www.liip.ch/en/services/content/trainings" rel="noreferrer" target="_blank">formations</a></li>
</ol>
<p>Les équipes éditoriales ont d’abord été constituées en fonction de la nouvelle structure du site: une partie thématique et une partie organisationnelle.</p>
<p>Pour la partie thématique, des équipes éditoriales par thème ont été créées, chacune avec un·e coordinateur·rice. Iels servent d’interface entre les équipes et la direction de projet. L’organisation de soutien (direction de projet + Liip) accompagne les équipes.</p>
<figure><img alt="" src="https://liip.rokka.io/www_inarticle_5/5713ad/gr-editorial-team-02-fr.jpg" srcset="https://liip.rokka.io/www_inarticle_5/o-dpr-2/5713ad/gr-editorial-team-02-fr.jpg 2x"></figure>
<p>Les formations ont constitué la première entrée pratique. Les nouveaux types de pages, les directives et le GR Editor y ont été présentés.</p>
<ol start="9">
<li>Accompagnement éditorial au quotidien</li>
</ol>
<p>Le travail de contenu est actuellement en cours. L’organisation de soutien anime des réunions éditoriales régulières, répond aux questions et développe des solutions. Chaque équipe thématique bénéficie d’un accompagnement individuel pour ses premiers textes.</p>
<ol start="10">
<li>Assurance qualité avant la mise en ligne</li>
</ol>
<p>Avec un tel volume de contenu, <strong>la responsabilité individuelle est essentielle</strong>. Les éditeur·rice·s disposent de check-lists pour relire leurs propres textes. Des contenus sélectionnés stratégiquement font l’objet de retours ciblés.</p>
<p>Le GR Editor apporte également une contribution majeure à l’assurance qualité.</p>
<h1>Premiers enseignements du projet</h1>
<p>Une refonte de cette ampleur met toutes les personnes concernées à l’épreuve. Un modèle éditorial décentralisé est inévitable. Comprendre la situation des membres des équipes éditoriales, faire preuve de flexibilité dans la mise en œuvre du plan et trouver des solutions pragmatiques en cours de route sont des éléments clés d’une collaboration constructive.</p>
<p>Concrètement, cela signifie par exemple:</p>
<ul>
<li>« Le ton fait la musique »: se traiter avec respect, aborder les points ouverts, prendre les problèmes au sérieux et favoriser la co-création.</li>
<li>Accorder une marge de manœuvre aux équipes éditoriales dans la planification, dans le cadre du calendrier global du projet.</li>
<li>Utiliser les outils de manière flexible (par exemple Excel au lieu de Miro si cela fonctionne mieux).</li>
</ul>
<p>Le travail avec <strong>des exemples de textes réels</strong> lors du développement des types de pages s’est clairement révélé être une décision clé. Avec autant de parties prenantes, <strong>des modèles aboutis et des références claires</strong> sont indispensables pour garantir qualité, cohérence et rapidité. Les visualisations des types de pages jouent également un rôle central. Pour les personnes qui pensent de manière très visuelle, rédiger dans des modèles Word sans aperçu du frontend est un défi. Les visualisations servent au moins de repère général.</p>
<p>Des expert·e·s externes peuvent aider à absorber les pics de charge et apporter un regard neuf aux discussions. Au-delà de cela, il est particulièrement gratifiant de voir émerger <strong>une équipe commune au-delà des frontières organisationnelles</strong>, qui collabore pour un objectif partagé.</p>
<p>C’est exactement dans cet esprit que nous nous réjouissons des prochaines, et dernières, étapes du processus, ainsi que d’une année passionnante pour la mise en ligne du nouveau site 🙂</p>]]></description>
    </item>
        <item>
      <title>Blind testing your chatbot, Arena style</title>
      <link>https://www.liip.ch/fr/blog/blind-testing-your-chatbot-arena-style</link>
      <guid>https://www.liip.ch/fr/blog/blind-testing-your-chatbot-arena-style</guid>
      <pubDate>Tue, 24 Feb 2026 00:00:00 +0100</pubDate>
      <description><![CDATA[<p>At Liip, we've been building and running AI-powered chatbots for organizations through our <a href="https://www.liip.ch/en/work/projects/liipgpt">LiipGPT</a> platform. Over time, we developed evaluation sets, automated scoring with LLM-as-a-Judge, and various ways to measure chatbot quality. Max wrote about this approach in <a href="https://www.liip.ch/en/blog/no-value-without-trust">No value without trust</a>. But when it comes to comparing two different configurations, like a new prompt versus the old one, or GPT-4o versus Claude Sonnet, automated metrics only get you so far. Sometimes you need actual humans reading actual answers and telling you which one is better.</p>
<h2>The Bias Problem</h2>
<p>The problem is bias. If you know that Response A comes from the expensive model and Response B from the cheaper one, you'll unconsciously read them differently. The idea behind our solution is borrowed from classic A/B testing: show two variants to evaluators without telling them which is which, and let the results speak. For RAG chatbots, the question isn't "which model is generally better?" It's "which configuration produces better answers for this specific knowledge base and these specific users?" That requires comparing pre-generated answers, not live model outputs.</p>
<h2>Side by Side, Let's Evaluate</h2>
<p>So we built Arena mode into the Admin UI. It works like this: you start by running your evaluation set against two different configurations. Maybe you're testing a new system prompt, or switching embedding models, or trying a different retrieval strategy. Each run produces answers for every question in the set. Then you create a comparison, selecting those two runs.</p>
<p>When an evaluator opens the comparison, they see one question at a time. Two answers, labeled A and B. No model names, no hints. The order is shuffled differently for each evaluator using a seeded randomization, so if Alice sees the Claude answer as "A" for question 3, Bob might see it as "B". This prevents evaluators from developing patterns like "A is always the better one."</p>
<p>For each answer, you rate it as Good, Neutral, or Bad. You can add a comment explaining why, which turns out to be incredibly valuable. The quantitative scores tell you which model wins, but the comments tell you why, and often reveal issues you wouldn't catch with automated evaluation.</p>
<figure><img alt="" src="https://liip.rokka.io/www_inarticle_5/3feb95/question-bad.jpg" srcset="https://liip.rokka.io/www_inarticle_5/o-dpr-2/3feb95/question-bad.jpg 2x"></figure>
<h2>More Feedback the Merrier</h2>
<p>Multiple evaluators can rate the same comparison independently. Getting people involved is easy: share links generate a temporary API key, so you can send a URL to a colleague or a client and they can evaluate in a protected area without needing an admin account. They just enter their name and start rating.</p>
<figure><img alt="" src="https://liip.rokka.io/www_inarticle_5/ade347/share.jpg" srcset="https://liip.rokka.io/www_inarticle_5/o-dpr-2/ade347/share.jpg 2x"></figure>
<h2>Understand the Results</h2>
<p>The results page shows a leaderboard: average score, win rate per model, and a breakdown per evaluator. That last part is where it gets interesting: it surfaces inter-annotator agreement, a standard measure of how much evaluators align in their ratings.</p>
<ul>
<li><strong>High agreement</strong>: the quality difference is clear and you can act on the result with confidence.</li>
<li><strong>Low agreement</strong>: the two configurations are close enough that the choice may come down to other factors like cost or latency.</li>
</ul>
<p>You can also drill into individual comments grouped by model, and export everything to Excel for reporting.</p>
<figure><img alt="" src="https://liip.rokka.io/www_inarticle_5/876023/arena-result.jpg" srcset="https://liip.rokka.io/www_inarticle_5/o-dpr-2/876023/arena-result.jpg 2x"></figure>
<h2>Calibrating AI with Human Truth</h2>
<p>Building this was a good reminder that evaluation tooling is never "done." Christian recently wrote about <a href="https://www.liip.ch/en/blog/using-claude-agent-sdk-to-analyse-chatbot-answers">using Claude Agent SDK to analyze chatbot answers</a> automatically, which is the other side of the coin: scaling evaluation with AI. Arena mode complements that by providing the human ground truth that automated evaluation needs to calibrate against.</p>
<p>We're using Arena now whenever we make significant changes to a chatbot's configuration. The signal-to-noise ratio is much better than staring at spreadsheets of automated scores. The feature is available in the Admin UI for any organization that uses our evaluation sets.</p>
<h2>The Next Steps of the Arena</h2>
<p>We're considering adding support for more than two models in a single comparison, and possibly integrating the Arena ratings as ground truth labels for training better LLM-as-a-Judge prompts. For now, blind human evaluation with a simple Good/Neutral/Bad rating scheme gives us exactly what we need: honest answers about which configuration actually works better.</p>]]></description>
    </item>
        <item>
      <title>WebMCP: Making LiipGPT Tools Discoverable by Browser AI Agents</title>
      <link>https://www.liip.ch/fr/blog/webmcp-making-liipgpt-tools-discoverable-by-browser-ai-agents</link>
      <guid>https://www.liip.ch/fr/blog/webmcp-making-liipgpt-tools-discoverable-by-browser-ai-agents</guid>
      <pubDate>Tue, 24 Feb 2026 00:00:00 +0100</pubDate>
      <description><![CDATA[<p>Two weeks ago, Google shipped <a href="https://developer.chrome.com/blog/webmcp-epp">WebMCP in Chrome 146</a> as an early preview. It's a W3C standard, jointly developed with Microsoft, that lets websites expose structured tools to AI agents running in the browser. Instead of agents scraping your DOM or clicking buttons, they can call well-defined functions with typed parameters and get structured results back. Think of it as MCP, but for the browser.</p>
<p>We already had an <a href="https://www.linkedin.com/posts/chregu_z%C3%BCricitygpt-and-in-extension-liipgpt-is-activity-7407037094528819200-oDdw">MCP server</a> running on <a href="https://zuericitygpt.ch/">ZüriCityGPT</a> and other <a href="https://www.liipgpt.ch/">LiipGPT</a> deployments. Tools like knowledge base search, public transport timetables, and waste collection schedules were available to any MCP client, Claude Desktop, Cursor, or custom integrations. The tool definitions, schemas, and execution logic were all there. WebMCP just needed a bridge to bring them into the browser.</p>
<p>The implementation turned out to be surprisingly straightforward. When the page loads, a small script checks if <code>navigator.modelContext</code> exists. If it does, it fetches the available tools from our existing MCP endpoint using a standard JSON-RPC <code>tools/list</code> request and registers each one with <code>navigator.modelContext.registerTool()</code>. When an AI agent calls a tool, the <code>execute</code> handler sends a <code>tools/call</code> request back to the same endpoint. The entire client-side code is about 30 lines of vanilla JavaScript.</p>
<pre><code class="language-javascript">navigator.modelContext.registerTool({
  name: tool.name,
  description: tool.description,
  inputSchema: tool.inputSchema,
  execute: function(params) {
    return fetch(mcpUrl, {
      method: 'POST',
      headers: { 'Content-Type': 'application/json' },
      body: JSON.stringify({
        jsonrpc: '2.0',
        method: 'tools/call',
        params: { name: tool.name, arguments: params }
      })
    }).then(r =&gt; r.json()).then(d =&gt; d.result);
  }
});</code></pre>
<p>The beauty is that the tool set is dynamic and per site setup. ZüriCityGPT exposes search with a special mode for city council resolutions (Stadtratsbeschlüsse, see also <a href="https://strb.zuericitygpt.ch/">StrbGPT</a>), waste collection lookups via the <a href="https://openerz.metaodi.ch/">OpenERZ API</a>, and Swiss public transport departures. A different LiipGPT deployment would expose its own set of tools. The backend decides what's available, the browser just registers whatever comes back.</p>
<p>One thing worth noting: <code>navigator.modelContext</code> is behind a flag in Chrome Canary (<code>chrome://flags/#enable-webmcp-testing</code>) and not available in any stable browser yet. The script uses feature detection, so nothing happens in unsupported browsers. </p>
<p>To actually test it, you need Chrome Canary and the <a href="https://chromewebstore.google.com/detail/model-context-tool-inspec/gbpdfapgefenggkahfehlcenpd">Model Context Tool Inspector</a> extension (which requires a Gemini API key to do anything useful with the tools). But the registration itself works, and seeing "search", "get_waste_collection", and "get_timetable_info" show up in the inspector when visiting a chatbot page is a nice confirmation that everything clicks together.</p>
<p>Now, ZüriCityGPT is a page with essentially one big input field. It's already a chatbot. An AI agent visiting the site could just type into that field. Exposing structured tools on a page that is itself a tool feels a little redundant. But it does open up genuinely new possibilities. A browser agent could search the knowledge base, check the next tram departure, and look up waste collection dates in parallel, without ever touching the chat UI, and combine the results with data from other sites. The chatbot answers questions one at a time. WebMCP lets agents compose capabilities.</p>
<p>Is this useful today? Not really. No stable browser supports it, and the user base of Chrome Canary with experimental flags enabled is, let's say, select. Google and Microsoft are pushing it through the W3C, and <a href="https://venturebeat.com/infrastructure/google-chrome-ships-webmcp-in-early-preview-turning-every-website-into-a">formal browser support is expected by mid-to-late 2026</a>. When that happens, any AI agent visiting ZüriCityGPT will automatically discover what it can do, no documentation reading and UI guessing required.</p>
<p>The interesting part is the convergence. We now have the same tool definitions serving three purposes: LangChain tools for the chatbot's own RAG pipeline, an MCP server for "traditional" AI clients, and WebMCP for browser agents. One set of schemas, three integration points. Adding a new tool to any LiipGPT site automatically makes it available everywhere.</p>
<p>If you want to try it yourself, visit <a href="https://zuericitygpt.ch/">zuericitygpt.ch</a> with Chrome Canary, enable the WebMCP flag, and install the <a href="https://chromewebstore.google.com/detail/model-context-tool-inspec/gbpdfapgefenggkahfehlcenpd">Model Context Tool Inspector</a> extension. You'll see the three registered tools and can ask Gemini when your next paper collection is. Or just type the question into the chatbot, that still works too.</p>]]></description>
    </item>
      </channel>
</rss>