<?xml version="1.0" encoding="utf-8"?><rss version="2.0">
  <channel>
    <title>Liip Blog</title>
    <link>https://www.liip.ch/de/blog</link>
    <lastBuildDate></lastBuildDate>
            <item>
      <title>Iframes are still odd</title>
      <link>https://www.liip.ch/de/blog/iframes-are-still-odd</link>
      <guid>https://www.liip.ch/de/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/de/blog/preventing-context-pollution-for-ai-agents</link>
      <guid>https://www.liip.ch/de/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>Der ConfIAnce-Chatbot, ein Jahr sp&#228;ter</title>
      <link>https://www.liip.ch/de/blog/der-confiance-chatbot-ein-jahr-spaeter</link>
      <guid>https://www.liip.ch/de/blog/der-confiance-chatbot-ein-jahr-spaeter</guid>
      <pubDate>Tue, 17 Mar 2026 00:00:00 +0100</pubDate>
      <description><![CDATA[<p>Vor etwas weniger als einem Jahr haben wir den ConfIAnce-Chatbot vorgestellt. Im Auftrag der Genfer Universitätskliniken (HUG) haben wir diesen Chatbot entwickelt, um einen einfachen und interaktiven Zugang zu medizinischen Informationen über häufige chronische Krankheiten zu ermöglichen. Die Inhalte werden von der medizinischen Institution selbst erstellt und validiert.</p>
<p>Ein Artikel der Initiator*innen dieses Projekts, veröffentlicht in der neuesten Ausgabe der Revue Médicale Suisse, zieht nun eine erste Bilanz ein Jahr nach der öffentlichen Lancierung.</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>Ein offizielles Angebot statt fehlerhafter Antworten online</h2>
<p>Die medizinische Grundversorgung, ein zentraler Pfeiler eines funktionierenden Gesundheitssystems, steht zunehmend unter Druck, selbst in Städten. Wenn Patient*innen ihre Hausärzt*innen nicht schnell erreichen können, suchen sie oft im Internet nach Antworten. Die Informationen, die sie dort finden, sind jedoch häufig ungenau oder sogar potenziell gefährlich.</p>
<p>In diesem Kontext kann eine gut konzipierte KI-Lösung helfen, <strong>die richtige Information zur richtigen Zeit</strong> bereitzustellen.</p>
<p>Deshalb haben wir die HUG bei der Entwicklung eines RAG-basierten Chatbots (Retrieval Augmented Generation) unterstützt. ConfIAnce ist nicht der erste Chatbot für Patient*innen. Er unterscheidet sich jedoch durch seine institutionelle Wurzeln, die Nutzung lokal validierter medizinischer Inhalte sowie durch Kontrollmechanismen, die die Zuverlässigkeit der Antworten sicherstellen.</p>
<p>Um Sicherheit zu gewährleisten, integriert das System mehrere Kontrollmechanismen, darunter <strong>Matching, Groundedness, Harmfulness, automatisierte Tests und semantisches Routing.</strong></p>
<h2>Qualitätssicherung durch Kontrolle des Systems</h2>
<p>Eine zentrale Herausforderung besteht darin, die Kontrolle über das System zu behalten. Dafür braucht es geeignete Monitoring-Funktionen.</p>
<p>Automatisierte Tests prüfen sämtliche Antworten des Chatbots auf ihre faktische Übereinstimmung mit der Wissensbasis.</p>
<p>Zusätzlich sorgt ein von Administrator*innen anpassbares Routing dafür, dass Fragen gefiltert und bei Bedarf unter menschlicher Aufsicht weitergeleitet werden. Sie können den Chatbot zudem sofort offline nehmen, falls ein Fehlverhalten vermutet wird.</p>
<p>Themen, über die häufig gefragt wird, aber in den Ausgangsdokumenten noch wenig behandelt werden, werden identifiziert und weiter ausgebaut. So wird die Wissensbasis im Rahmen eines <strong>kontinuierlichen Verbesserungsprozesses erweitert</strong>.</p>
<h2>Selbst das beste Tool bringt nur etwas, wenn es genutzt wird</h2>
<p>Damit Patient*innen den Chatbot tatsächlich verwenden, haben die HUG eine öffentliche Informationskampagne gestartet. Ziel war es, das Tool bekannt zu machen und gleichzeitig realistische Erwartungen zu setzen.</p>
<p>ConfIAnce ist <strong>kein medizinisches Gerät und ersetzt keinen Arztbesuch</strong>. Vielmehr ist er ein Informationsangebot, das Fragen zu den häufigsten chronischen Erkrankungen bei Erwachsenen beantwortet.</p>
<p>Im Februar 2025 ging ConfIAnce in der Beta-Version online. Zwischen Anfang Februar und Ende Oktober 2025 haben 3’823 Nutzer*innen mit dem Chatbot interagiert. Daraus entstanden 5’969 Gespräche und <strong>11’781 Fragen</strong>, im Durchschnitt etwa zwei Fragen pro Gespräch.</p>
<p>Die direkt im Chatbot abgegebenen Rückmeldungen sind zu <strong>75 % positiv</strong>.</p>
<h2>Hohe Akzeptanz für einen etwas anderen Chatbot</h2>
<p>Chatbots im Gesundheitsbereich werden von Patient*innen meist gut angenommen, vor allem wegen ihrer ständigen Verfügbarkeit und der einfachen Nutzung. Studien zeigen jedoch auch wiederkehrende Probleme: eine uneinheitliche Qualität der Antworten sowie mangelnde Transparenz bei den verwendeten Quellen.</p>
<p>Genau hier unterscheidet sich ConfIAnce von vielen anderen medizinischen Chatbots.<br />
Der Chatbot wurde entwickelt, um <strong>die Beziehung zwischen Patient<em>innen und Ärzt\</em>innen zu unterstützen, nicht zu ersetzen</strong>. ConfIAnce entlastet Ärzt*innen in der Grundversorgung und schafft ihnen mehr Zeit für die menschliche Seite ihres Berufs.</p>
<p>Die Autor<em>innen des Artikels in der Revue Médicale Suisse betonen zudem, dass der Chatbot, der im spezifischen Kontext der HUG und ihrer Dokumente entwickelt wurde, auch in anderen institutionellen Umgebungen eingesetzt werden kann. Damit ein solches Projekt erfolgreich ist, braucht es zunächst hochwertige Daten, wie sie hier zur Verfügung standen. Darüber hinaus ermöglichen Kontrollmechanismen, automatisierte Tests und Nutzer\</em>innen-Feedback eine kontinuierliche Verbesserung – und sorgen für Sicherheit und Relevanz, die Vertrauen schaffen.</p>]]></description>
    </item>
        <item>
      <title>LiipGPT barrierefrei machen: Unsere Reise zur WCAG-AA-Konformit&#228;t</title>
      <link>https://www.liip.ch/de/blog/liipgpt-barrierefrei-gestalten</link>
      <guid>https://www.liip.ch/de/blog/liipgpt-barrierefrei-gestalten</guid>
      <pubDate>Mon, 16 Mar 2026 00:00:00 +0100</pubDate>
      <description><![CDATA[<p>Nachdem wir uns zuerst auf die Themenbasiertheit unseres Chatbots <a href="https://www.liipgpt.ch/" rel="noreferrer" target="_blank">LiipGPT</a> konzentriert hatten (zuletzt präsentiert beim <a href="https://zuericitygpt.ch/" rel="noreferrer" target="_blank">Z&uuml;riCityGPT Relaunch</a>), richteten wir unsere Aufmerksamkeit auf Barrierefreiheit mit dem Ziel, die WCAG-AA-Konformität zu erreichen. Wie bei vielen Funktionen haben wir zuerst untersucht, wie Branchenführende wie ChatGPT, Perplexity und Claude mit Barrierefreiheit umgehen. Obwohl wir überall Verbesserungspotenzial festgestellt haben, inspirierte uns das, darüber nachzudenken, wie wir es besser machen können.</p>
<p>Unsere Reise zur Barrierefreiheit folgte vier Hauptschritten: automatische Scans und Quick-Fixes, Tastaturnavigation, Optimierung für Mobile-Zoom und anschliessend für den Screen-Reader.</p>
<h2>Automatische Scans und Quick-Fixes</h2>
<p>Wir begannen mit automatisierten Accessibility-Tests mithilfe von Browser-Erweiterungen wie <a href="https://chromewebstore.google.com/detail/ibm-equal-access-accessib/lkcagbfjnkomcinoddgooolagloogehp" rel="noreferrer" target="_blank">IBM Equal Access Accessibility Checker</a> und <a href="https://www.deque.com/axe/devtools/extension" rel="noreferrer" target="_blank">axe DevTools</a>. Diese Tools halfen uns, häufige Probleme zu identifizieren: fehlende Labels, unzureichender Farbkontrast, unsauberes semantisches HTML und fehlende ARIA-Attribute. Obwohl automatisierte Scans nur etwa 40% der Accessibility-Probleme erkennen, boten sie eine solide Grundlage für unsere Arbeit.</p>
<h2>Tastaturnavigation</h2>
<p>Eine korrekte Tastaturnavigation ist grundlegend für die Barrierefreiheit. Sicherzustellen, dass die grundlegende Tab-Navigation in der gesamten App funktioniert, ist relativ einfach. Komplexere Komponenten wie <a href="https://www.w3.org/WAI/ARIA/apg/patterns/tabs/examples/tabs-automatic/" rel="noreferrer" target="_blank">Tabs</a>, <a href="https://www.w3.org/WAI/ARIA/apg/patterns/menubar/" rel="noreferrer" target="_blank">Men&uuml;s</a> und <a href="https://www.w3.org/WAI/ARIA/apg/patterns/dialog-modal/" rel="noreferrer" target="_blank">Modals</a> erfordern jedoch erweiterte Tastaturinteraktionen: Pfeiltasten, Escape-Handling und Fokus-Management gemäss den offiziellen W3C-Richtlinien. Nutzer*innen, die auf Tastaturnavigation angewiesen sind, erwarten diese spezifischen Muster. Wenn davon abgewichen wird, führt das zu Verwirrung und Frustration. Anstatt diese Muster von Grund auf neu zu implementieren, nutzten wir <a href="https://bits-ui.com/" rel="noreferrer" target="_blank">Bits UI</a>, eine Headless-UI-Bibliothek, die diese Accessibility-Richtlinien korrekt umsetzt.</p>
<p>Über einzelne Komponenten hinaus implementierten wir Fokus-Loops und Fokus-Wiederherstellung auf Anwendungsebene, damit Nutzer*innen beim Wechsel zwischen verschiedenen Bereichen der Chat-Oberfläche stets orientiert bleiben.</p>
<h2>Optimierung für Mobile-Zoom</h2>
<p>Während User-Tests für <a href="https://meinplatz.ch/" rel="noreferrer" target="_blank">meinplatz.ch</a> mit Nutzer*innen mit Behinderungen beobachteten wir etwas Auffälliges: Viele navigieren auf Websites auf mobilen Geräten mit 200% oder mehr Zoom und halten ihre Geräte nur etwa 10 cm vor die Augen. Diese Erkenntnis zeigte eine kritische Lücke in den meisten Chatbot-Implementierungen auf.</p>
<p>Die meisten Chatbots verwenden Elemente mit fixer Position: ein Chat-Input am unteren Rand und häufig eine Kopfzeile am oberen Rand. Wenn Nutzer*innen stark hineinzoomen, können fixierte Elemente den gesamten Viewport einnehmen und die Oberfläche unbenutzbar machen. Leider ist es in Browsern nicht möglich, das Zoom-Level zuverlässig zu erkennen. Unsere Lösung: Wir verwenden den Intersection Observer, um zu erkennen, wenn Header oder Footer mehr Platz einnehmen als erwartet. In diesem Fall entfernen wir ihre fixe Positionierung dynamisch, um die Nutzbarkeit wiederherzustellen.</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>Elemente mit fester Position verursachen Probleme in vergr&ouml;sserten Ansichtsfenstern.</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>L&ouml;sung: Feststehende Elemente wieder auf statische Positionierung zur&uuml;cksetzen, sobald ein Zoom erkannt wird.</figcaption></figure>
<h2>Screen-Reader-Erfahrung</h2>
<p>Barrierefreiheit für Screen-Reader entsteht nicht automatisch, sie erfordert sorgfältiges Design. Wir konzentrierten uns darauf, klaren Kontext durch eine saubere Seitenstruktur (Landmarks und Überschriften) bereitzustellen. So verstehen Nutzer*innen jederzeit, wo sie sich befinden und was gerade passiert, und erhalten Shortcuts zu den wichtigsten Bereichen der App.</p>
<h4>Kontext bereitstellen</h4>
<p>Wir implementierten eine umfassende Outline-Struktur mit Landmarks für die Hauptnavigation, die Einstellungen und die Eingabebereiche. Jede Nachricht enthält korrekte Überschriften und Labels. Zusätzlich haben wir nach dem Chat-Input (am unteren Seitenrand) einen Skip-Link eingefügt, der Nutzer*innen hilft, schnell zum Seitenanfang zurückzukehren.</p>
<h4>Herausforderungen mit Web Components</h4>
<p>Die Arbeit mit Web Components brachte eigene Herausforderungen mit sich. VoiceOver reagiert besonders empfindlich darauf, wie Bibliotheken implementiert sind. Wir arbeiteten eng mit dem Bits-UI-Team zusammen (das sehr schnell auf Bug-Reports reagierte) und implementierten beispielsweise lokale Portals für Dropdown-Menüs, um Navigationsprobleme mit VoiceOver zu vermeiden.</p>
<h4>Umgang mit Ankündigungen</h4>
<p>Eine der schwierigsten Herausforderungen war das Management von VoiceOver-Ankündigungen, wenn mehrere Ereignisse gleichzeitig auftraten. Da das Queueing von Ankündigungen nicht zuverlässig funktioniert, haben wir Ereignisse sorgfältig sequenziert und zusammengehörige Ankündigungen zusammengeführt. Wenn Nutzer*innen zum Beispiel „Alle Optionen auswählen“ in einer Liste anklicken, würden normalerweise einzelne Ankündigungen für jede Option ausgelöst und sich gegenseitig überschreiben. Stattdessen brechen wir diese einzelnen Ankündigungen ab und ersetzen sie durch eine einzige klare Meldung, die alles zusammenfasst (alle Elemente ausgewählt oder abgewählt, auf die vordefinierte Auswahl zurückgesetzt usw.).</p>
<p>Da der Chat eine SPA ohne Seiten-Reloads ist, war es zudem wichtig, alle Änderungen anzukündigen, die nur visuell sichtbar sind, zum Beispiel: Wechsel zwischen Light- und Dark-Mode, Sprachwechsel usw.</p>
<h4>Chat-Flow für Screen Reader</h4>
<p>Wir haben die Chat-Erfahrung speziell für Screen-Reader-Nutzer*innen gestaltet:</p>
<ul>
<li>Das Eingabefeld enthält sowohl einen Placeholder als auch ein aria-label mit dem Seitentitel. Dadurch wird beim Laden der Seite Kontext bereitgestellt, da das Eingabefeld automatisch fokussiert wird und Nutzer*innen den initialen Seiteninhalt überspringen.</li>
<li>Wenn eine Antwort generiert wird, kündigen wir dies klar an und geben damit dasselbe Feedback wie bei einer visuellen Ladeanzeige.</li>
<li>Sobald eine Antwort bereit ist, wird sie ohne Markdown-Formatierung vorgelesen (kein Kursiv, keine Links usw.), um einen natürlichen Lesefluss zu gewährleisten.</li>
<li>Nach dem Vorlesen einer Antwort weisen wir darauf hin, dass Nutzer*innen direkt eine weitere Frage stellen oder zu den Optionen der letzten Nachricht navigieren können, um Feedback zu geben oder Referenzen anzusehen. Wir fügen diesen interaktiven Abschnitt der letzten Nachricht dynamisch zur Dokument-Outline hinzu und schaffen damit einen schnellen Navigations-Shortcut.</li>
<li>Der Chat-Verlauf ist als Artikel mit Labels strukturiert, sodass frühere Konversationen leicht navigierbar sind.</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>L&ouml;sung: Navigation im Chatbot mit dem Bildschirmleseprogramm VoiceOver auf macOS.</figcaption></figure>
<h2>Selbst ausprobieren</h2>
<p>Du kannst diese Verbesserungen mit <a href="https://www.bs.ch/alva" rel="noreferrer" target="_blank">Alva</a> erleben, dem Chatbot der Verwaltung Basel-Stadt. Versuche, mit <a href="https://www.google.com/search?q=how+to+navigate+a+website+with+voiceover" rel="noreferrer" target="_blank">VoiceOver (MacOS)</a> oder <a href="https://www.google.com/search?q=how+to+navigate+a+website+with+nva+screen+reader" rel="noreferrer" target="_blank">NVA (Windows)</a> zu navigieren, nur deine Tastatur zu verwenden oder auf einem mobilen Gerät stark hineinzuzoomen.</p>
<h2>Eine fortlaufende Reise</h2>
<p>Unser nächstes Ziel ist es, automatisierte Accessibility-Tests in unsere CI-Pipeline zu integrieren. Wie bereits erwähnt, erkennen automatisierte Scans jedoch nur rund 40 % der Accessibility-Probleme. Das bedeutet, dass wir weiterhin jede neue Funktion sorgfältig planen und manuell testen müssen. Nichts ersetzt menschliches Testen, wenn es um Barrierefreiheit geht. Automatisierte Tools können fehlende Labels oder Kontrastprobleme markieren, aber sie können nicht beurteilen, ob eine Oberfläche tatsächlich für jemanden nutzbar ist, der mit Screen-Reader oder Tastatur navigiert.</p>
<p>Barrierefreiheit ist eine fortlaufende Reise, kein Ziel. Wir setzen uns dafür ein, LiipGPT für alle nutzbar zu machen, und werden unseren Ansatz kontinuierlich auf Basis von Praxisfeedback weiterentwickeln.</p>
<h2>Brauchst du Unterstützung bei deiner Accessibility?</h2>
<p>Wir bieten Accessibility-Audits an, um Probleme in deinen Anwendungen zu identifizieren und zu beheben. Wenn du die Barrierefreiheit deines Produkts verbessern möchtest, <a href="https://www.liip.ch/en/contact">kontaktiere uns</a>, wir helfen gerne weiter.</p>]]></description>
    </item>
        <item>
      <title>Einblicke in KI und Open Source f&#252;r Beh&#246;rden bei Drupal4Gov</title>
      <link>https://www.liip.ch/de/blog/einblicke-in-ki-und-open-source-fuer-behoerden-bei-drupal4gov</link>
      <guid>https://www.liip.ch/de/blog/einblicke-in-ki-und-open-source-fuer-behoerden-bei-drupal4gov</guid>
      <pubDate>Wed, 11 Mar 2026 00:00:00 +0100</pubDate>
      <description><![CDATA[<p>Die Drupal4Gov-Konferenz war vollgepackt mit spannenden Vorträgen. Ich war auch dort, um unsere Arbeit am Projekt Kanton Basel-Stadt / Alva / blökkli vorzustellen. Darüber haben wir bereits geschrieben, aber es gibt bereits wieder neue Funktionen.</p>
<h2>GovNL: Von Monaten zu Minuten beim Aufbau von Websites</h2>
<p>GovNL kombiniert Open-Source-Drupal-Komponenten mit einem offenen Designsystem, um zahlreiche Websites der niederländischen Regierung zugänglich und skalierbar zu betreiben. Die Zeit für den Aufbau neuer Websites reduziert sich dadurch <strong>von etwa drei Monaten auf rund zehn Minuten</strong>. Beeindruckend, um es gelinde zu sagen. Das ist ein starkes Beispiel dafür, wie Design für Wiederverwendung in grossem Massstab funktionieren kann.</p>
<h2>Europäische Kommission: Koordination ist der Schlüssel zur Skalierung</h2>
<p>Die Europäische Kommission betreibt bereits <strong>770 Websites</strong> und investiert stark in das Drupal-Ökosystem. Besonders aufgefallen ist mir, wie stark der Fokus auf <strong>Koordination</strong> liegt. Man muss sicherstellen, dass die richtigen Inhalte über den richtigen Kanal innerhalb dieser grossen Website-Landschaft veröffentlicht werden. Open Source Program Offices (OSPOs) wurden eingerichtet, um Open-Source-Strategien sowohl auf Regierungsebene als auch innerhalb von Organisationen voranzutreiben.</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>Website des Kantons Basel-Stadt und Alva: ein Blueprint für lokale öffentliche Verwaltungen</h2>
<p>Kurz vor der Mittagspause war ich an der Reihe, <strong>die verschiedenen KI-Use-Cases</strong> vorzustellen, die wir für den <a href="https://www.liip.ch/de/work/projects/basel-stadt">Kanton Basel-Stadt</a> umgesetzt haben. Der Kanton setzte mit dem Relaunch von bs.ch neue Standards: mit einem nutzerorientierten Design, einem themenbasierten Zugang statt interner Organisationsstruktur und <a href="https://www.bs.ch/alva">Alva</a> als <strong>erstem KI-basierten Chatbot für einen Schweizer Kanton</strong>. Der Tech-Stack basiert auf Open-Source-Komponenten, und Liip hat im Rahmen des Relaunches stark zu Open Source beigetragen. Wir verwenden Drupal als CMS, Nuxt/Vue, den <a href="https://blokk.li/">bl&ouml;kkli-Editor</a> für das Headless-Frontend und Elasticsearch für die Suche. Die Inhalte werden von einem departementsübergreifenden Redaktionsteam erstellt, das einer klaren Content-Strategie folgt.</p>
<h2>KI zur Unterstützung der Öffentlichkeit und der Editor*innen</h2>
<p>Der Vortrag war auch eine Gelegenheit, mehr als 18 Monate nach dem Go-Live Zahlen zu teilen. Heute beantwortet Alva <strong>über 10 000 Fragen pro Monat</strong>, mit durchschnittlich <strong>1,36 Fragen pro Konversation und einem Wachstum von +44 % seit Alva 2.0</strong>. Dank API-Integrationen kann der Chatbot Fragen auf Basis von Echtzeitinformationen beantworten. Alva wird stark von internen Mitarbeitenden des Kantons sowie von der Öffentlichkeit genutzt. Der Chatbot zeigt und überprüft stets seine Quellen, was entscheidend ist, um Vertrauen aufzubauen.</p>
<p>Auf der Redaktionsseite arbeitet blökkli intensiv daran, Texte zu vereinfachen. Mit dem <strong>blökkli-Editor</strong> und integrierter KI können Editor*innen nun Lesbarkeitsanalysen durchführen, vorgeschlagene Vereinfachungen nebeneinander sehen und diese übernehmen oder anpassen. Alva und die KI-Funktionen auf bs.ch werden kontinuierlich weiterentwickelt, um Editor*innen und Bürger*innen vertrauenswürdige KI-Technologien bereitzustellen.</p>
<h2>KI-gestützte Technologien in der französischen Regierung</h2>
<p>Ein weiterer inspirierender Vortrag zeigte Use-Cases von KI auf der Plattform Services Publics+ der französischen Regierung. Mit über 140 000 geteilten Erfahrungen und mehr als einer Million Reaktionen nutzt das System KI-gestützte Technologien, um staatlichen Stellen dabei zu helfen, Feedback an Bürger*innen zu geben. Dazu gehören Speech-to-Text und Echtzeit-Zusammenfassungen als zentrale Technologien. C’est magnifique!</p>
<h2>Die EU vertraut Open Source mehr denn je</h2>
<p>Die Europäische Union hat den <strong>Website Evidence Collector</strong> eingeführt – ein Open-Source-Tool, das Websites auf Sicherheitsprobleme überprüft. Bemerkenswert ist, dass das Projekt unter der <strong>EUPL</strong> (European Union Public Licence) veröffentlicht wurde. Diese betont die Interoperabilität zwischen Ländern und Lizenzen und unterstützt mehrsprachige Zusammenarbeit. Ich frage mich, ob es in der Schweiz etwas Ähnliches gibt.</p>
<p>Mit Interoperable Europe bietet die EU ausserdem ein neues Portal mit einem hilfreichen <strong>Licensing Assistant</strong>. Damit kannst du <a href="https://interoperable-europe.ec.europa.eu/collection/eupl/solution/licensing-assistant/find-and-compare-software-licenses">Softwarelizenzen finden und vergleichen</a> sowie mit einem <a href="https://interoperable-europe.ec.europa.eu/collection/eupl/solution/licensing-assistant/compatibility-checker">Kompatibilit&auml;ts-Checker</a> prüfen, ob verschiedene Open-Source-Lizenzen kombiniert werden können und ob rechtliche Komplikationen entstehen könnten.</p>
<h2>Open Source zu nutzen reicht nicht - wir brauchen Champions</h2>
<p>Zum Abschluss betonte <strong>Tiffany Farris</strong> von der Strategieberatung <a href="https://www.palantir.net/">Palantir.net</a> (nicht zu verwechseln mit Palantir Technologies), dass <strong>die Nutzung von Open Source gut ist,  aber nicht ausreicht</strong>. Organisationen brauchen <strong>Champions</strong>, die Beiträge zur Community und die Gesundheit des Ökosystems aktiv vorantreiben. Design für Wiederverwendung sollte ein zentrales Prinzip sein. Aus US-Perspektive ist Beschaffung (Procurement) ein grosses Problem: Die Nutzung von Open Source wächst, aber Unterstützungsmechanismen oft nicht. Wenn Open Source als „kostenlos“ betrachtet wird, kann das dazu führen, dass Aufträge an Anbieter gehen, die ihre Arbeit zwar als Open Source vermarkten, aber kein nachhaltiges Ökosystem unterstützen. Sie schlug konkrete Anpassungen der öffentlichen Beschaffung im Sinne von <strong>„Public Money, Public Code“</strong> vor, um das Ökosystem besser zu unterstützen. Ein wirklich inspirierender Abschluss für einen intensiven Tag voller Lernen und Austausch.</p>
<p>Wenn du tiefer in die Inhalte eintauchen möchtest, kannst du dir <a href="https://www.youtube.com/playlist?list=PLNubpNMwP36QH5Y3RlbOiV4f9hjlrxCOo">die Playlist</a> der Präsentationen von Drupal4Gov EU 2026 ansehen.</p>]]></description>
    </item>
        <item>
      <title>Web Components: The Good, the Bad, and the Ugly</title>
      <link>https://www.liip.ch/de/blog/web-components-the-good-the-bad-and-the-ugly</link>
      <guid>https://www.liip.ch/de/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/de/blog/city-of-zurich-s-900-open-data-sets-now-have-an-mcp-server</link>
      <guid>https://www.liip.ch/de/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>Der Kanton Graub&#252;nden bekommt eine neue Website</title>
      <link>https://www.liip.ch/de/blog/der-kanton-graubuenden-bekommt-eine-neue-website</link>
      <guid>https://www.liip.ch/de/blog/der-kanton-graubuenden-bekommt-eine-neue-website</guid>
      <pubDate>Wed, 25 Feb 2026 00:00:00 +0100</pubDate>
      <description><![CDATA[<p>Der Kanton Graubünden entwickelt eine neue Website. Die heutige Struktur orientiert sich am Aufbau der Verwaltung und umfasst mehrere 10 000 Webseiten. Der Auftritt wird den aktuellen Anforderungen an Benutzerfreundlichkeit und dem veränderten Nutzungsverhalten nicht mehr gerecht.</p>
<p>Im Zentrum des Relaunchs steht die <strong>Nutzerzentrierung</strong>. Statt der Innenperspektive soll eine themenzentrierte Struktur entstehen – wie sie beispielsweise der Kanton Basel-Stadt (<a href="https://www.liip.ch/de/work/projects/basel-stadt" rel="noreferrer" target="_blank">mehr zum Projekt hier</a>) bereits erfolgreich etabliert hat.</p>
<p><strong>Das Vorhaben aus Content-Sicht? Ein Mammut-Projekt.</strong></p>
<h2>Was braucht es für ein Content-Projekt dieser Grösse?</h2>
<p>Organisch gewachsene Inhalte können nicht einfach migriert werden. Ein Relaunch ist meist der Moment, in dem Content nicht nur übertragen, sondern <strong>qualitativ neu gedacht</strong> wird.</p>
<p>Da dabei ein grosses Volumen an Content überarbeitet werden muss, braucht es ein <strong>dezentrales Redaktionsmodell</strong>. Zahlreiche Mitarbeitende – oft ohne vertiefte Content-Erfahrung – sind involviert. Gute Prozesse, klare Rollen und gezielte Unterstützung sind daher essenziell:</p>
<ul>
<li>Eine entscheidungsfreudige Projektleitung </li>
<li>Eine solide Bestandesaufnahme</li>
<li>Konzept und Strategie</li>
<li>Governance (Prozesse &amp; Verantwortlichkeiten)</li>
<li>Aufbau und Schulung der Webredaktion</li>
<li>Operative Content-Produktion</li>
<li>Qualitätssicherung</li>
</ul>
<p>Liip darf den Kanton Graubünden auf dieser Reise begleiten. Auf Basis unserer Erfahrungen aus den Projekten in Basel-Stadt, Solothurn und verschiedenen Bundesämtern ist eine vertrauensvolle, effiziente Zusammenarbeit entstanden.</p>
<h2>Wie wir zusammenarbeiten</h2>
<p>Zwei <a href="https://www.liip.ch/de/services/content/strategic-storytelling" rel="noreferrer" target="_blank">Content-Strateginnen</a> von Liip arbeiten eng mit der Projektleitung des Kantons zusammen. Die Projektleitung bringt internes Know-how und Entscheidungsstärke ein, Liip ergänzt operative Kapazität und viel Erfahrung aus ähnlichen Settings. Dieses Setup ermöglicht hohe Geschwindigkeit – und eine saubere Verzahnung von internem Wissen und externem Input.</p>
<p>Die Zusammenarbeit erstreckt sich über mehr als 1,5 Jahre.</p>
<h2>Was sind die konkreten Lieferobjekte?</h2>
<ul>
<li>Content Audit</li>
<li>Content Strategie &amp; Tonalität</li>
<li>Content Governance &amp; Redaktionsmodell (für die Projektphase und für den Betrieb danach)</li>
<li>Content Lifecycle Modell</li>
<li>Content Guidelines für die Redaktion</li>
<li>Seitentypen &amp; Beispieltexte (in Zusammenarbeit mit dem Design-Team)</li>
<li>Wordvorlagen für die Erstellung der Texte</li>
<li>Schulungen &amp; Support der Redaktionsmitglieder</li>
<li>KI-basiertes Schreibtool «GR-Editor» (basierend auf TextMate)</li>
<li>Qualitätssicherung</li>
</ul>
<figure><img alt="" src="https://liip.rokka.io/www_inarticle_5/1496b9/gr-work-layers-01-de.jpg" srcset="https://liip.rokka.io/www_inarticle_5/o-dpr-2/1496b9/gr-work-layers-01-de.jpg 2x"></figure>
<h3>Was bedeutet das im Detail? Vom Audit bis zur Qualitätssicherung</h3>
<p>Ein Relaunch mit über 120 involvierten Redaktor*innen und Tausenden Seiten Content braucht vor allem eines: einen klaren Plan für die Umsetzung und gute Vorbereitung. Jeder Schritt baut auf dem vorherigen auf:</p>
<ol>
<li><a href="https://www.liip.ch/de/services/content/content-audit" rel="noreferrer" target="_blank">Content Audit</a> – Klarheit schaffen</li>
</ol>
<p>Gestartet sind wir in das Projekt mit der Analyse einer repräsentativen Auswahl bestehender Inhalte. Der Audit half bei der Identifizierung von Optimierungspotenzialen und bildete das Fundament für alle weiteren Entscheidungen.</p>
<ol start="2">
<li>Das Redaktionsmodell – Rollen, Verantwortlichkeiten und Prozesse</li>
</ol>
<p>Die <strong><a href="https://www.liip.ch/de/services/content/content-governance" rel="noreferrer" target="_blank">Content Governance</a> und das entwickelte Redaktionsmodell</strong> haben einen grossen Teil der «Theorie» gelegt, auf der die operativen Arbeiten mit der Webredaktion dann aufgebaut wurden. Wir haben dabei ein Modell für das Projekt entworfen, ergänzt um einen Plan für nach dem Relaunch.</p>
<ol start="3">
<li>Content Strategie &amp; Tonalität – die Leitlinie</li>
</ol>
<p>Die Governance regelt Zusammenarbeit und Prozesse – die Content Strategie definiert die inhaltliche Ausrichtung. Sie richtet sich dabei weniger an die künftigen Redaktor*innen, sondern bildet die konzeptionelle und inhaltliche Grundlage für die operativen Elemente. Ergänzt wurden <strong>zwei Tonalitäten</strong> für den neuen Webauftritt.</p>
<p>Mit abgeschlossener Strategie begann die operative Phase.</p>
<ol start="4">
<li>Seitentypen &amp; Beispieltexte – das Konzept wird konkret</li>
</ol>
<p>Gemeinsam mit dem Designteam entwickelten wir die neuen Seitentypen. Unsere <a href="https://www.liip.ch/de/services/content/ux-writing" rel="noreferrer" target="_blank">UX Writerin</a> entwarf erste realistische Seiten, die als Referenz für die Finalisierung der Seitentypen dienten.</p>
<p>Arbeiten mit echten Texten ist entscheidend: Nur so lassen sich Struktur, Inhalt und Design sinnvoll aufeinander abstimmen.</p>
<p>Auch für die Schulung der Redaktor*innen sind die konkreten Text- und Seitenbeispiele wichtig.</p>
<ol start="5">
<li><a href="https://www.liip.ch/de/services/content/guidelines" rel="noreferrer" target="_blank">Content Guidelines</a> – das Handbuch für Redaktionsmitglieder</li>
</ol>
<p>Abgeleitet von der Content Strategie bündeln die Content Guidelines Regeln, Empfehlungen, Beispiele und konkrete Anleitungen – für verständliche, barrierefreie und konsistente Inhalte.</p>
<ol start="6">
<li>Wordvorlagen – weil das CMS parallel erst entsteht</li>
</ol>
<p>Da CMS-Entwicklung und Content-Entwicklung parallel laufen, müssen die Texte ausserhalb des CMS entstehen. Dafür erstellten wir Wordvorlagen, die:</p>
<ul>
<li>die neue Seitenstruktur abbilden,</li>
<li>alle relevanten Meta-Informationen integrieren,</li>
<li>Hinweise für Redaktor*innen enthalten.</li>
</ul>
<p>Die Wordvorlagen wurden um Visualisierungen der einzelnen Seitentypen ergänzt. Dabei wurde ein <strong>reales Textbeispiel pro Seitentyp in Figma gesetzt</strong> und mit den Redaktor*innen als PDF geteilt.</p>
<ol start="7">
<li>KI-Schreibtool «GR-Editor» – Unterstützung in Echtzeit</li>
</ol>
<p>Ein dezentrales Redaktionsmodell führt zwangsläufig zu variierender Textqualität. Der GR-Editor (basierend auf <a href="https://www.liip.ch/de/blog/textmate" rel="noreferrer" target="_blank">TextMate</a>) wirkt dem entgegen. Er prüft <strong>Verständlichkeit, Konsistenz und Tonalität</strong> direkt beim Schreiben.</p>
<p>Der Kanton hatte ideale Voraussetzungen: bestehende kantonale Schreibvorgaben, die neuen Content Guidelines und die zwei definierten Tonalitäten.</p>
<ol start="8">
<li>Aufbau der Redaktionen &amp; <a href="https://www.liip.ch/de/services/content/trainings" rel="noreferrer" target="_blank">Schulungen</a></li>
</ol>
<p>Zunächst wurden die Redaktionen zusammengestellt, passend zur neuen Website-Struktur: einem Themen- und einem Organisationsteil.</p>
<p>Für den Thementeil entstanden Themenredaktionen mit je einer Koordinator*in. Sie funktionieren als <strong>Schnittstelle zwischen den Redaktionen und der Projektleitung</strong>. Die Supportorganisation (Projektleitung + Liip) begleitet die Teams.</p>
<figure><img alt="" src="https://liip.rokka.io/www_inarticle_5/7226da/gr-editorial-team-02-de.jpg" srcset="https://liip.rokka.io/www_inarticle_5/o-dpr-2/7226da/gr-editorial-team-02-de.jpg 2x"></figure>
<p>Die Schulungen waren der erste praktische Einstieg. Vorgestellt wurden die neuen Seitentypen, die Guidelines und der GR-Editor.</p>
<ol start="9">
<li>Redaktionssupport – Begleitung im Alltag</li>
</ol>
<p>Die inhaltlichen Arbeiten laufen derzeit. Die Supportorganisation organisiert regelmässige Redaktionssitzungen, beantwortet Fragen und <strong>erarbeitet Lösungen</strong>. Jede Themenredaktion erhält individuelles Sparring zu ersten Texten.</p>
<ol start="10">
<li>Qualitätssicherung – bevor der Inhalt live geht</li>
</ol>
<p>Bei dieser Menge an Inhalten ist <strong>Eigenverantwortung</strong> zentral. Die Redaktor*innen erhalten Checklisten, mit denen sie ihre Texte selbst prüfen können. Strategisch ausgewählte Inhalte erhalten spezifisches Feedback.</p>
<p>Auch der GR-Editor trägt wesentlich zur Qualitätssicherung bei.</p>
<h2>Erste Learnings aus dem Projekt</h2>
<p>Ein Relaunch dieser Grösse strapaziert alle Beteiligten. Ein dezentrales Redaktionsmodell ist unvermeidlich. Verständnis für die Situation der Redaktionsmitglieder, Flexibilität bei der Umsetzung der Planung und pragmatische Lösungen unterwegs sind zentrale Zutaten für ein konstruktives Miteinander.  </p>
<p>Was bedeutet das konkret? Einige Beispiele:</p>
<ul>
<li>«Der Ton macht die Musik». Sich mit Respekt begegnen, offene Punkte benennen, Probleme ernst nehmen, Mitgestaltung zulassen.</li>
<li>Den Redaktionen Gestaltungsspielraum für ihr Timing geben – innerhalb des übergeordneten Projektzeitplans.</li>
<li>Tools flexibel einsetzen (z. B. Excel statt Miro, wenn es besser passt).</li>
</ul>
<p>Die Arbeit <strong>mit realen Textbeispielen in der Entwicklung der Seitentypen</strong> hat sich klar als wichtige Entscheidung erwiesen. Bei so vielen Beteiligten braucht es reife Vorlagen und klare Referenzen – um Qualität, Konsistenz und Tempo sicherzustellen. Auch die <strong>Visualisierungen der Seitentypen</strong> haben einen zentralen Stellenwert. Gerade für Menschen, die stark über das Visuelle funktionieren, ist das Texten in den Wordvorlagen ohne Vorschau des Frontends eine Herausforderung. Die Visualisierungen dienen immerhin als allgemeiner Referenzwert.</p>
<p>Externe Expert*innen können Spitzen brechen und in Diskussionen einen externen Blick beisteuern. Darüber hinaus ist es schön zu sehen, wie ein gemeinsames Team über Organisationsgrenzen hinaus entstehen kann - gemeinsam im Sinn der Sache.</p>
<p>..und genau in diesem Sinne freuen wir uns auf die nächsten, abschliessenden Prozessschritte. Und auf ein spannendes Jahr für den Relaunch :)</p>]]></description>
    </item>
        <item>
      <title>Blind testing your chatbot, Arena style</title>
      <link>https://www.liip.ch/de/blog/blind-testing-your-chatbot-arena-style</link>
      <guid>https://www.liip.ch/de/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/de/blog/webmcp-making-liipgpt-tools-discoverable-by-browser-ai-agents</link>
      <guid>https://www.liip.ch/de/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>