<?xml version="1.0" encoding="utf-8"?><feed xmlns="http://www.w3.org/2005/Atom" ><generator uri="https://jekyllrb.com/" version="3.10.0">Jekyll</generator><link href="https://www.amitmerchant.com/feed.xml" rel="self" type="application/atom+xml" /><link href="https://www.amitmerchant.com/" rel="alternate" type="text/html" /><updated>2026-10-09T15:36:11+00:00</updated><id>https://www.amitmerchant.com/feed.xml</id><title type="html">Amit Merchant</title><subtitle>A blog on PHP, JavaScript, and more</subtitle><author><name>Amit Merchant</name></author><entry><title type="html">A Practical Way to Check Your AI Search Visibility</title><link href="https://www.amitmerchant.com/practical-way-to-check-your-ai-search-visibility/" rel="alternate" type="text/html" title="A Practical Way to Check Your AI Search Visibility" /><published>2026-10-07T00:00:00+00:00</published><updated>2026-10-07T00:00:00+00:00</updated><id>https://www.amitmerchant.com/practical-way-to-check-your-ai-search-visibility</id><content type="html" xml:base="https://www.amitmerchant.com/practical-way-to-check-your-ai-search-visibility/"><![CDATA[<p>Last month, I built <a href="/building-web-searching-ai-agent-javascript/">an AI agent that could search the web</a> before answering a question. While working on the article around that project, I got another project idea and that was around the visibility of a website in the world of AI-powered search.</p>

<p>The idea essentially revolves around the fact that whether the AI-powered search mentions your website in its answer or not or whether it cites your website as a source or not, and lastly, compare these resulst with the regular Google search results.</p>

<link rel="preconnect" href="https://fonts.googleapis.com" />

<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin="" />

<link href="https://fonts.googleapis.com/css2?family=Rampart+One&amp;display=swap" rel="stylesheet" />

<style>
  .tag-question {
    font-family: "Rampart One", cursive;
    text-align: center;
    text-wrap: balance;
    font-size: 1.6em;
    margin: 35px 18px;
    background: 
      /* Top-left corner */
      linear-gradient(to right, #121212 2px, transparent 2px) no-repeat left top,
      linear-gradient(to bottom, #121212 2px, transparent 2px) no-repeat left top,
      /* Top-right corner */
      linear-gradient(to left, #121212 2px, transparent 2px) no-repeat right top,
      linear-gradient(to bottom, #121212 2px, transparent 2px) no-repeat right top,
      /* Bottom-left corner */
      linear-gradient(to right, #121212 2px, transparent 2px) no-repeat left bottom,
      linear-gradient(to top, #121212 2px, transparent 2px) no-repeat left bottom,
      /* Bottom-right corner */
      linear-gradient(to left, #121212 2px, transparent 2px) no-repeat right bottom,
      linear-gradient(to top, #121212 2px, transparent 2px) no-repeat right bottom;
    background-size: 20px 20px;
    padding: 22px 2px;
  }
</style>

<p class="tag-question">
    Does your website show up when AI-powered search answers questions related to the topics it covers?
</p>

<p>When you search something on a search engine, you get a list of results. And based on that you can decide the ranking of your website. That’s not true with the AI-generated answers. Because these answers are generated based on the information available on the web and the AI model’s understanding of the topic.</p>

<p>So, if your website is not mentioned in the answer or cited as a source, it can be difficult to know whether your website is visible to the AI model or not.</p>

<p>In this tutorial, we’ll build a small AI search visibility analyzer tool in JavaScript to tackle the above challenge. The tool can accept a domain and multiple queries, call Google AI Mode and regular Google search through SearchApi, and compare the two responses.</p>

<ul id="markdown-toc">
  <li><a href="#what-were-going-to-build" id="markdown-toc-what-were-going-to-build">What we’re going to build</a></li>
  <li><a href="#prerequisites" id="markdown-toc-prerequisites">Prerequisites</a></li>
  <li><a href="#the-application-architecture" id="markdown-toc-the-application-architecture">The application architecture</a></li>
  <li><a href="#calling-searchapi-from-the-backend" id="markdown-toc-calling-searchapi-from-the-backend">Calling SearchApi from the backend</a></li>
  <li><a href="#understanding-the-two-response-shapes" id="markdown-toc-understanding-the-two-response-shapes">Understanding the two response shapes</a></li>
  <li><a href="#turning-ai-mode-into-readable-answer-text" id="markdown-toc-turning-ai-mode-into-readable-answer-text">Turning AI Mode into readable answer text</a></li>
  <li><a href="#normalizing-the-target-domain" id="markdown-toc-normalizing-the-target-domain">Normalizing the target domain</a></li>
  <li><a href="#detecting-ai-answer-mentions" id="markdown-toc-detecting-ai-answer-mentions">Detecting AI answer mentions</a></li>
  <li><a href="#inspecting-ai-references" id="markdown-toc-inspecting-ai-references">Inspecting AI references</a></li>
  <li><a href="#comparing-regular-google-results" id="markdown-toc-comparing-regular-google-results">Comparing regular Google results</a></li>
  <li><a href="#returning-a-stable-application-response" id="markdown-toc-returning-a-stable-application-response">Returning a stable application response</a></li>
  <li><a href="#running-both-searches-for-a-query" id="markdown-toc-running-both-searches-for-a-query">Running both searches for a query</a></li>
  <li><a href="#building-the-form" id="markdown-toc-building-the-form">Building the form</a></li>
  <li><a href="#running-the-application" id="markdown-toc-running-the-application">Running the application</a></li>
  <li><a href="#taking-this-further" id="markdown-toc-taking-this-further">Taking this further</a></li>
  <li><a href="#in-closing" id="markdown-toc-in-closing">In closing</a></li>
</ul>

<p>If you’ve read my previous article, you would know that the project was about giving a terminal-based application a real-time web-search capability. This project uses that same premise to inspect how AI-search results are presented to users.</p>

<p>The finished application will be built around the following architecture.</p>

<p><img src="/images/ai-analyzer-flowchart.png" alt="AI Analyzer Flowchart" /></p>

<h2 id="what-were-going-to-build">What we’re going to build</h2>

<p>Our AI search visibility analyzer has 2 inputs.</p>

<ul>
  <li>A domain, such as <code class="language-plaintext highlighter-rouge">css-tricks.com</code>.</li>
  <li>A list of queries, one query per line.</li>
</ul>

<p>For every query, it’ll show the following information:</p>

<ul>
  <li>The Google AI Mode answer.</li>
  <li>Whether the target domain is mentioned in that answer.</li>
  <li>The sources cited by AI Mode.</li>
  <li>Other domains appearing in those citations.</li>
  <li>Regular Google’s organic search results.</li>
  <li>Whether the target domain appears in those results.</li>
  <li>The best organic position when Google provides one.</li>
</ul>

<p>The tool intentionally doesn’t give a single <em>“AI visibility score”</em> here.</p>

<p>That’s because being mentioned in an answer or being cited as a source or appearing in an organic result can be related, but they are essentially not the same thing. A single number out of these would be misleading and would not make any sense.</p>

<p>On top of this, it’s also a one-time analyzer. It does not store runs, draw historical charts, or schedule recurring checks. These features can be enhancements for the next iteration of the project.</p>

<h2 id="prerequisites">Prerequisites</h2>

<p>To build the project, you will need…</p>

<ul>
  <li>Node.js 20 or later installled on your machine.</li>
  <li>A <a href="https://www.searchapi.io?utm_source=Dev&amp;utm_medium=Ambassador&amp;utm_campaign=amitmerchant.com">SearchApi</a> API key.</li>
  <li>A browser to run the frontend.</li>
</ul>

<p>Create a project and initialize it with npm.</p>

<div class="language-sh highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="nb">mkdir </span>ai-analyzer
<span class="nb">cd </span>ai-analyzer
npm init <span class="nt">-y</span>
</code></pre></div></div>

<p>The project uses Node’s built-in <code class="language-plaintext highlighter-rouge">fetch</code>, HTTP server, test runner, and environment-file support. So, you do not need to install any additional dependencies.</p>

<p>Next, we’ll create a <code class="language-plaintext highlighter-rouge">.env</code> file at the project root and add our SearchApi API key.</p>

<div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code>SEARCHAPI_API_KEY="your_searchapi_api_key"
</code></pre></div></div>

<p>This API key will stay on the server. We’re not placing it in client-side JavaScript or sending it back in an API response. That could be a security hazard.</p>

<h2 id="the-application-architecture">The application architecture</h2>

<p>The application has a small Node.js backend and a static frontend and the entire package could be summarized like so.</p>

<div class="language-text highlighter-rouge"><div class="highlight"><pre class="highlight"><code>Browser
  → POST /api/analyze
      → validate domain and queries
      → call Google AI Mode
      → call regular Google
      → normalize provider responses
      → compare domains and aggregate signals
Server
  ← return a stable application response
</code></pre></div></div>

<p>We’re keeping the SearchApi-specific details out of the UI.</p>

<p>SearchApi returns different data for AI Mode and regular Google search results. And so, in the backend, we’re turning those responses into a simple format that the frontend can understand.</p>

<p>So, the project is split into the following few small pieces/modules:</p>

<ul>
  <li><code class="language-plaintext highlighter-rouge">server.mjs</code> - The entry point of the application. It handles the HTTP server and routes.</li>
  <li><code class="language-plaintext highlighter-rouge">src/searchapi.mjs</code> - Handles the SearchApi calls for both Google AI Mode and regular Google search.</li>
  <li><code class="language-plaintext highlighter-rouge">src/analysis.mjs</code> - Handles the analysis logic to compare the AI Mode and regular Google search results.</li>
  <li><code class="language-plaintext highlighter-rouge">public/</code> - Contains the static frontend files.</li>
  <li><code class="language-plaintext highlighter-rouge">test/</code> - Contains the test files for the backend modules.</li>
</ul>

<h2 id="calling-searchapi-from-the-backend">Calling SearchApi from the backend</h2>

<p>The <a href="https://www.searchapi.io/docs/google-ai-mode-api?utm_source=Dev&amp;utm_medium=Ambassador&amp;utm_campaign=amitmerchant.com">SearchApi</a> request pattern is the same one I used in the previous article, which is <strong>to construct a URL</strong>, <strong>add the engine and query</strong>, and <strong>send the API key as a bearer token</strong>.</p>

<p>Here, we’ll be using two SearchApi engines to fulfill our requirements.</p>

<ul>
  <li><code class="language-plaintext highlighter-rouge">google_ai_mode</code></li>
  <li><code class="language-plaintext highlighter-rouge">google</code></li>
</ul>

<p>We kept the SearchApi calls in their own module so the rest of the application doesn’t have to worry about how the requests are made.</p>

<p>Here’s what the module looks like.</p>

<div class="language-js highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="kd">const</span> <span class="nx">endpoint</span> <span class="o">=</span> <span class="dl">"</span><span class="s2">https://www.searchapi.io/api/v1/search</span><span class="dl">"</span><span class="p">;</span>

<span class="k">export</span> <span class="k">async</span> <span class="kd">function</span> <span class="nx">searchApi</span><span class="p">({</span>
  <span class="nx">engine</span><span class="p">,</span>
  <span class="nx">query</span><span class="p">,</span>
  <span class="nx">apiKey</span><span class="p">,</span>
  <span class="nx">fetchImpl</span> <span class="o">=</span> <span class="nx">fetch</span><span class="p">,</span>
  <span class="nx">timeoutMs</span> <span class="o">=</span> <span class="mi">30</span><span class="nx">_000</span><span class="p">,</span>
<span class="p">})</span> <span class="p">{</span>
  <span class="k">if</span> <span class="p">(</span><span class="o">!</span><span class="nx">apiKey</span><span class="p">)</span> <span class="p">{</span>
    <span class="k">throw</span> <span class="k">new</span> <span class="nb">Error</span><span class="p">(</span><span class="dl">"</span><span class="s2">SEARCHAPI_API_KEY is not configured.</span><span class="dl">"</span><span class="p">);</span>
  <span class="p">}</span>

  <span class="kd">const</span> <span class="nx">url</span> <span class="o">=</span> <span class="k">new</span> <span class="nx">URL</span><span class="p">(</span><span class="nx">endpoint</span><span class="p">);</span>
  <span class="nx">url</span><span class="p">.</span><span class="nx">searchParams</span><span class="p">.</span><span class="kd">set</span><span class="p">(</span><span class="dl">"</span><span class="s2">engine</span><span class="dl">"</span><span class="p">,</span> <span class="nx">engine</span><span class="p">);</span>
  <span class="nx">url</span><span class="p">.</span><span class="nx">searchParams</span><span class="p">.</span><span class="kd">set</span><span class="p">(</span><span class="dl">"</span><span class="s2">q</span><span class="dl">"</span><span class="p">,</span> <span class="nx">query</span><span class="p">);</span>

  <span class="kd">const</span> <span class="nx">controller</span> <span class="o">=</span> <span class="k">new</span> <span class="nx">AbortController</span><span class="p">();</span>
  <span class="kd">const</span> <span class="nx">timer</span> <span class="o">=</span> <span class="nx">setTimeout</span><span class="p">(()</span> <span class="o">=&gt;</span> <span class="nx">controller</span><span class="p">.</span><span class="nx">abort</span><span class="p">(),</span> <span class="nx">timeoutMs</span><span class="p">);</span>

  <span class="k">try</span> <span class="p">{</span>
    <span class="kd">const</span> <span class="nx">response</span> <span class="o">=</span> <span class="k">await</span> <span class="nx">fetchImpl</span><span class="p">(</span><span class="nx">url</span><span class="p">,</span> <span class="p">{</span>
      <span class="na">headers</span><span class="p">:</span> <span class="p">{</span>
        <span class="na">Authorization</span><span class="p">:</span> <span class="s2">`Bearer </span><span class="p">${</span><span class="nx">apiKey</span><span class="p">}</span><span class="s2">`</span><span class="p">,</span>
        <span class="na">Accept</span><span class="p">:</span> <span class="dl">"</span><span class="s2">application/json</span><span class="dl">"</span><span class="p">,</span>
      <span class="p">},</span>
      <span class="na">signal</span><span class="p">:</span> <span class="nx">controller</span><span class="p">.</span><span class="nx">signal</span><span class="p">,</span>
    <span class="p">});</span>

    <span class="k">if</span> <span class="p">(</span><span class="o">!</span><span class="nx">response</span><span class="p">.</span><span class="nx">ok</span><span class="p">)</span> <span class="p">{</span>
      <span class="k">throw</span> <span class="k">new</span> <span class="nb">Error</span><span class="p">(</span><span class="s2">`SearchApi returned </span><span class="p">${</span><span class="nx">response</span><span class="p">.</span><span class="nx">status</span><span class="p">}</span><span class="s2">.`</span><span class="p">);</span>
    <span class="p">}</span>

    <span class="k">return</span> <span class="k">await</span> <span class="nx">response</span><span class="p">.</span><span class="nx">json</span><span class="p">();</span>
  <span class="p">}</span> <span class="k">finally</span> <span class="p">{</span>
    <span class="nx">clearTimeout</span><span class="p">(</span><span class="nx">timer</span><span class="p">);</span>
  <span class="p">}</span>
<span class="p">}</span>

<span class="k">export</span> <span class="kd">const</span> <span class="nx">searchAiMode</span> <span class="o">=</span> <span class="p">(</span><span class="nx">options</span><span class="p">)</span> <span class="o">=&gt;</span>
  <span class="nx">searchApi</span><span class="p">({</span> <span class="p">...</span><span class="nx">options</span><span class="p">,</span> <span class="na">engine</span><span class="p">:</span> <span class="dl">"</span><span class="s2">google_ai_mode</span><span class="dl">"</span> <span class="p">});</span>

<span class="k">export</span> <span class="kd">const</span> <span class="nx">searchGoogle</span> <span class="o">=</span> <span class="p">(</span><span class="nx">options</span><span class="p">)</span> <span class="o">=&gt;</span>
  <span class="nx">searchApi</span><span class="p">({</span> <span class="p">...</span><span class="nx">options</span><span class="p">,</span> <span class="na">engine</span><span class="p">:</span> <span class="dl">"</span><span class="s2">google</span><span class="dl">"</span> <span class="p">});</span>
</code></pre></div></div>

<p>As you can tell, we’re using <strong>URL</strong> and <strong>searchParams</strong> to construct the request rather than concatenating the query into the URL ourselves. This takes care of encoding things like spaces and punctuation.</p>

<p>Since we’re dealing with an external API, we also added a timeout to the request. If the request takes longer than 30 seconds, it will be <a href="/abort-fetch-request-manually-in-javascript/">aborted</a> and an error will be thrown.</p>

<h2 id="understanding-the-two-response-shapes">Understanding the two response shapes</h2>

<p>The Google AI Mode and the regular Google Search returns different response structures. So, we need to handle them a little differently.</p>

<p>For instance, the following fields can be returned in the AI mode response:</p>

<ul>
  <li><code class="language-plaintext highlighter-rouge">text_blocks</code> - An array of text blocks that make up the answer. Each block can be a string or an object with an <code class="language-plaintext highlighter-rouge">answer</code> field and an optional <code class="language-plaintext highlighter-rouge">items</code> array.</li>
  <li><code class="language-plaintext highlighter-rouge">markdown</code> - A string containing the answer in Markdown format.</li>
  <li><code class="language-plaintext highlighter-rouge">reference_links</code> - An array of links to the sources cited by the answer.</li>
  <li><code class="language-plaintext highlighter-rouge">web_results</code> - An array of additional web results.</li>
</ul>

<p>On the other hand, the regular Google Search returns <code class="language-plaintext highlighter-rouge">organic_results</code> for the usual search listings.</p>

<p>I didn’t want the application to deal with all these different fields itself and so, I created a normalization function for both the response types to extract the relevant information and return back it in one consistent format.</p>

<div class="language-js highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="kd">function</span> <span class="nx">normalizeWebResults</span><span class="p">(</span><span class="nx">response</span><span class="p">)</span> <span class="p">{</span>
  <span class="kd">const</span> <span class="nx">results</span> <span class="o">=</span>
    <span class="nx">response</span><span class="p">?.</span><span class="nx">organic_results</span> <span class="o">??</span>
    <span class="nx">response</span><span class="p">?.</span><span class="nx">web_results</span> <span class="o">??</span>
    <span class="p">[];</span>

  <span class="k">return</span> <span class="p">(</span><span class="nb">Array</span><span class="p">.</span><span class="nx">isArray</span><span class="p">(</span><span class="nx">results</span><span class="p">)</span> <span class="p">?</span> <span class="nx">results</span> <span class="p">:</span> <span class="p">[])</span>
    <span class="p">.</span><span class="nx">map</span><span class="p">(</span><span class="nx">normalizeLink</span><span class="p">)</span>
    <span class="p">.</span><span class="nx">filter</span><span class="p">((</span><span class="nx">item</span><span class="p">)</span> <span class="o">=&gt;</span> <span class="nx">item</span><span class="p">.</span><span class="nx">link</span> <span class="o">||</span> <span class="nx">item</span><span class="p">.</span><span class="nx">title</span> <span class="o">||</span> <span class="nx">item</span><span class="p">.</span><span class="nx">snippet</span><span class="p">);</span>
<span class="p">}</span>
</code></pre></div></div>

<p>Here, we’re falling back to <code class="language-plaintext highlighter-rouge">web_results</code> because AI Mode can also return web results. We’re not treating the two response formats as identical. We’re just extracting the parts that the analyzer needs into a common format.</p>

<p>Also, these fields are optional. That is because a query might not return any references or organic results, and that’s quite different from the request failing itself. So, we need to handle the case where these fields are missing and return an empty array instead of throwing an error.</p>

<h2 id="turning-ai-mode-into-readable-answer-text">Turning AI Mode into readable answer text</h2>

<p>We’re using SearchApi’s <code class="language-plaintext highlighter-rouge">text_blocks</code> instead of the <code class="language-plaintext highlighter-rouge">markdown</code> field because the <code class="language-plaintext highlighter-rouge">markdown</code> field can contain citation links and reference markers that don’t look great when simply printed on the page.</p>

<p>For the first version, I decided to extract the actual text from <code class="language-plaintext highlighter-rouge">text_blocks</code> instead like so.</p>

<div class="language-js highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="kd">function</span> <span class="nx">answerText</span><span class="p">(</span><span class="nx">response</span><span class="p">)</span> <span class="p">{</span>
  <span class="kd">const</span> <span class="nx">blocks</span> <span class="o">=</span> <span class="nb">Array</span><span class="p">.</span><span class="nx">isArray</span><span class="p">(</span><span class="nx">response</span><span class="p">?.</span><span class="nx">text_blocks</span><span class="p">)</span>
    <span class="p">?</span> <span class="nx">response</span><span class="p">.</span><span class="nx">text_blocks</span>
    <span class="p">:</span> <span class="p">[];</span>

  <span class="kd">const</span> <span class="nx">text</span> <span class="o">=</span> <span class="nx">blocks</span>
    <span class="p">.</span><span class="nx">map</span><span class="p">((</span><span class="nx">block</span><span class="p">)</span> <span class="o">=&gt;</span> <span class="p">{</span>
      <span class="k">if</span> <span class="p">(</span><span class="k">typeof</span> <span class="nx">block</span> <span class="o">===</span> <span class="dl">"</span><span class="s2">string</span><span class="dl">"</span><span class="p">)</span> <span class="p">{</span>
        <span class="k">return</span> <span class="nx">block</span><span class="p">;</span>
      <span class="p">}</span>

      <span class="kd">const</span> <span class="nx">parts</span> <span class="o">=</span> <span class="p">[</span>
        <span class="nx">block</span><span class="p">?.</span><span class="nx">answer</span><span class="p">,</span>
        <span class="p">...(</span><span class="nb">Array</span><span class="p">.</span><span class="nx">isArray</span><span class="p">(</span><span class="nx">block</span><span class="p">?.</span><span class="nx">items</span><span class="p">)</span>
          <span class="p">?</span> <span class="nx">block</span><span class="p">.</span><span class="nx">items</span><span class="p">.</span><span class="nx">map</span><span class="p">((</span><span class="nx">item</span><span class="p">)</span> <span class="o">=&gt;</span> <span class="nx">item</span><span class="p">?.</span><span class="nx">answer</span><span class="p">)</span>
          <span class="p">:</span> <span class="p">[]),</span>
      <span class="p">];</span>

      <span class="k">return</span> <span class="nx">parts</span><span class="p">.</span><span class="nx">filter</span><span class="p">(</span><span class="nb">Boolean</span><span class="p">).</span><span class="nx">join</span><span class="p">(</span><span class="dl">"</span><span class="se">\n</span><span class="dl">"</span><span class="p">);</span>
    <span class="p">})</span>
    <span class="p">.</span><span class="nx">filter</span><span class="p">(</span><span class="nb">Boolean</span><span class="p">)</span>
    <span class="p">.</span><span class="nx">join</span><span class="p">(</span><span class="dl">"</span><span class="se">\n\n</span><span class="dl">"</span><span class="p">);</span>

  <span class="k">return</span> <span class="nx">text</span> <span class="o">||</span> <span class="nx">response</span><span class="p">?.</span><span class="nx">markdown</span> <span class="o">||</span> <span class="dl">""</span><span class="p">;</span>
<span class="p">}</span>
</code></pre></div></div>

<p>As you can tell, we’re iterating through the <code class="language-plaintext highlighter-rouge">text_blocks</code> array and extracting the <code class="language-plaintext highlighter-rouge">answer</code> field from each block. If a block has an <code class="language-plaintext highlighter-rouge">items</code> array, we’re also extracting the <code class="language-plaintext highlighter-rouge">answer</code> field from each item in that array.</p>

<p>Lastly, we’re joining all the extracted answers with two newlines to create a readable answer text. If for some reason, there are no <code class="language-plaintext highlighter-rouge">text_blocks</code>, we’re falling back to the <code class="language-plaintext highlighter-rouge">markdown</code> field.</p>

<h2 id="normalizing-the-target-domain">Normalizing the target domain</h2>

<p>Before comparing the results, we need to make sure the domain entered by the user is in a consistent format. For instance, the following URLs should point to the same target.</p>

<div class="language-text highlighter-rouge"><div class="highlight"><pre class="highlight"><code>css-tricks.com
https://css-tricks.com
https://www.css-tricks.com/some-page
</code></pre></div></div>

<p>So instead of comparing the full URLs, we’re extracting the hostname and using that for the comparison.</p>

<div class="language-js highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="k">export</span> <span class="kd">function</span> <span class="nx">normalizeDomain</span><span class="p">(</span><span class="nx">value</span><span class="p">)</span> <span class="p">{</span>
  <span class="kd">const</span> <span class="nx">input</span> <span class="o">=</span> <span class="nb">String</span><span class="p">(</span><span class="nx">value</span> <span class="o">??</span> <span class="dl">""</span><span class="p">).</span><span class="nx">trim</span><span class="p">();</span>

  <span class="k">if</span> <span class="p">(</span><span class="o">!</span><span class="nx">input</span><span class="p">)</span> <span class="p">{</span>
    <span class="k">throw</span> <span class="k">new</span> <span class="nb">Error</span><span class="p">(</span><span class="dl">"</span><span class="s2">Enter a domain.</span><span class="dl">"</span><span class="p">);</span>
  <span class="p">}</span>

  <span class="kd">const</span> <span class="nx">candidate</span> <span class="o">=</span> <span class="sr">/^https</span><span class="se">?</span><span class="sr">:</span><span class="se">\/\/</span><span class="sr">/i</span><span class="p">.</span><span class="nx">test</span><span class="p">(</span><span class="nx">input</span><span class="p">)</span>
    <span class="p">?</span> <span class="nx">input</span>
    <span class="p">:</span> <span class="s2">`https://</span><span class="p">${</span><span class="nx">input</span><span class="p">}</span><span class="s2">`</span><span class="p">;</span>

  <span class="kd">let</span> <span class="nx">hostname</span><span class="p">;</span>

  <span class="k">try</span> <span class="p">{</span>
    <span class="nx">hostname</span> <span class="o">=</span> <span class="k">new</span> <span class="nx">URL</span><span class="p">(</span><span class="nx">candidate</span><span class="p">)</span>
      <span class="p">.</span><span class="nx">hostname</span>
      <span class="p">.</span><span class="nx">toLowerCase</span><span class="p">()</span>
      <span class="p">.</span><span class="nx">replace</span><span class="p">(</span><span class="sr">/^www</span><span class="se">\.</span><span class="sr">/</span><span class="p">,</span> <span class="dl">""</span><span class="p">);</span>
  <span class="p">}</span> <span class="k">catch</span> <span class="p">{</span>
    <span class="k">throw</span> <span class="k">new</span> <span class="nb">Error</span><span class="p">(</span><span class="dl">"</span><span class="s2">Enter a valid domain, such as example.com.</span><span class="dl">"</span><span class="p">);</span>
  <span class="p">}</span>

  <span class="k">if</span> <span class="p">(</span>
    <span class="o">!</span><span class="nx">hostname</span> <span class="o">||</span>
    <span class="nx">hostname</span><span class="p">.</span><span class="nx">includes</span><span class="p">(</span><span class="dl">"</span><span class="s2"> </span><span class="dl">"</span><span class="p">)</span> <span class="o">||</span>
    <span class="o">!</span><span class="nx">hostname</span><span class="p">.</span><span class="nx">includes</span><span class="p">(</span><span class="dl">"</span><span class="s2">.</span><span class="dl">"</span><span class="p">)</span> <span class="o">||</span>
    <span class="nx">hostname</span><span class="p">.</span><span class="nx">startsWith</span><span class="p">(</span><span class="dl">"</span><span class="s2">.</span><span class="dl">"</span><span class="p">)</span> <span class="o">||</span>
    <span class="nx">hostname</span><span class="p">.</span><span class="nx">endsWith</span><span class="p">(</span><span class="dl">"</span><span class="s2">.</span><span class="dl">"</span><span class="p">)</span>
  <span class="p">)</span> <span class="p">{</span>
    <span class="k">throw</span> <span class="k">new</span> <span class="nb">Error</span><span class="p">(</span><span class="dl">"</span><span class="s2">Enter a valid domain, such as example.com.</span><span class="dl">"</span><span class="p">);</span>
  <span class="p">}</span>

  <span class="k">return</span> <span class="nx">hostname</span><span class="p">;</span>
<span class="p">}</span>
</code></pre></div></div>

<p>As you can see, we’re also not treating every subdomain as the same domain. <code class="language-plaintext highlighter-rouge">blog.example.com</code> and <code class="language-plaintext highlighter-rouge">example.com</code> might be two different properties, and so we want to keep them separate. The only exception is the <code class="language-plaintext highlighter-rouge">www</code> subdomain, which is treated as the same as the root domain.</p>

<h2 id="detecting-ai-answer-mentions">Detecting AI answer mentions</h2>

<p>When you work with AI-generated answers, there are two possibilities. Either the answer mentions a website as a useful resource or it cites a website as one of its sources.</p>

<p>Here, we’re using a simple string check to if the answer mentions the target domain or not.</p>

<div class="language-js highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="kd">const</span> <span class="nx">answer</span> <span class="o">=</span> <span class="nx">answerText</span><span class="p">(</span><span class="nx">aiResponse</span><span class="p">);</span>

<span class="kd">const</span> <span class="nx">mentionedDomain</span> <span class="o">=</span> <span class="nx">answer</span>
  <span class="p">.</span><span class="nx">toLowerCase</span><span class="p">()</span>
  <span class="p">.</span><span class="nx">includes</span><span class="p">(</span><span class="nx">targetDomain</span><span class="p">);</span>
</code></pre></div></div>

<p>As you can tell, we’re converting both the answer and the target domain to lowercase before checking for a mention. This makes the check case-insensitive.</p>

<h2 id="inspecting-ai-references">Inspecting AI references</h2>

<p>Next, we’re normalizing the AI Mode references so that we can work with them in the same way throughout the application using a <code class="language-plaintext highlighter-rouge">normalizeLink</code> function.</p>

<div class="language-js highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="kd">function</span> <span class="nx">normalizeLink</span><span class="p">(</span><span class="nx">item</span><span class="p">)</span> <span class="p">{</span>
  <span class="kd">const</span> <span class="nx">link</span> <span class="o">=</span> <span class="k">typeof</span> <span class="nx">item</span><span class="p">?.</span><span class="nx">link</span> <span class="o">===</span> <span class="dl">"</span><span class="s2">string</span><span class="dl">"</span>
    <span class="p">?</span> <span class="nx">item</span><span class="p">.</span><span class="nx">link</span>
    <span class="p">:</span> <span class="kc">undefined</span><span class="p">;</span>

  <span class="k">return</span> <span class="p">{</span>
    <span class="na">title</span><span class="p">:</span> <span class="nx">item</span><span class="p">?.</span><span class="nx">title</span> <span class="o">||</span> <span class="kc">undefined</span><span class="p">,</span>
    <span class="nx">link</span><span class="p">,</span>
    <span class="na">hostname</span><span class="p">:</span> <span class="nx">hostnameFromUrl</span><span class="p">(</span><span class="nx">link</span><span class="p">),</span>
    <span class="na">source</span><span class="p">:</span> <span class="nx">item</span><span class="p">?.</span><span class="nx">source</span> <span class="o">||</span> <span class="kc">undefined</span><span class="p">,</span>
    <span class="na">snippet</span><span class="p">:</span> <span class="nx">item</span><span class="p">?.</span><span class="nx">snippet</span> <span class="o">||</span> <span class="kc">undefined</span><span class="p">,</span>
    <span class="na">displayedLink</span><span class="p">:</span> <span class="nx">item</span><span class="p">?.</span><span class="nx">displayed_link</span> <span class="o">||</span> <span class="kc">undefined</span><span class="p">,</span>
    <span class="na">position</span><span class="p">:</span> <span class="nb">Number</span><span class="p">.</span><span class="nb">isFinite</span><span class="p">(</span><span class="nx">item</span><span class="p">?.</span><span class="nx">position</span><span class="p">)</span>
      <span class="p">?</span> <span class="nx">item</span><span class="p">.</span><span class="nx">position</span>
      <span class="p">:</span> <span class="kc">undefined</span><span class="p">,</span>
    <span class="na">index</span><span class="p">:</span> <span class="nb">Number</span><span class="p">.</span><span class="nb">isFinite</span><span class="p">(</span><span class="nx">item</span><span class="p">?.</span><span class="nx">index</span><span class="p">)</span>
      <span class="p">?</span> <span class="nx">item</span><span class="p">.</span><span class="nx">index</span>
      <span class="p">:</span> <span class="kc">undefined</span><span class="p">,</span>
  <span class="p">};</span>
<span class="p">}</span>
</code></pre></div></div>

<p>Here, we’re extracting the <code class="language-plaintext highlighter-rouge">hostname</code> from the link so that we can compare it with the target domain. We’re also normalizing other fields like <code class="language-plaintext highlighter-rouge">title</code>, <code class="language-plaintext highlighter-rouge">source</code>, and <code class="language-plaintext highlighter-rouge">snippet</code> to make sure they are either strings or undefined.</p>

<p>Once the references are in this format, checking whether the target domain was cited is quite straightforward. We just check if any of the normalized references have a hostname that matches the target domain like so.</p>

<div class="language-js highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="kd">const</span> <span class="nx">citedTarget</span> <span class="o">=</span> <span class="nx">references</span><span class="p">.</span><span class="nx">some</span><span class="p">(</span>
  <span class="p">(</span><span class="nx">reference</span><span class="p">)</span> <span class="o">=&gt;</span> <span class="nx">reference</span><span class="p">.</span><span class="nx">hostname</span> <span class="o">===</span> <span class="nx">targetDomain</span>
<span class="p">);</span>
</code></pre></div></div>

<p>One thing I noticed while working with the references is that some Google links can be redirects rather than direct links to the original page. So, when SearchApi can’t resolve one of those links, we keep the URL as it is instead of trying to guess where it points.</p>

<h2 id="comparing-regular-google-results">Comparing regular Google results</h2>

<p>The regular Google search gives us the traditional organic results, so we can use those to check whether the target domain appears there and if so, what its best position is.</p>

<div class="language-js highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="kd">const</span> <span class="nx">results</span> <span class="o">=</span> <span class="nx">normalizeWebResults</span><span class="p">(</span><span class="nx">googleResponse</span><span class="p">);</span>

<span class="kd">const</span> <span class="nx">matchingResults</span> <span class="o">=</span> <span class="nx">results</span><span class="p">.</span><span class="nx">filter</span><span class="p">(</span>
  <span class="p">(</span><span class="nx">result</span><span class="p">)</span> <span class="o">=&gt;</span> <span class="nx">result</span><span class="p">.</span><span class="nx">hostname</span> <span class="o">===</span> <span class="nx">targetDomain</span>
<span class="p">);</span>

<span class="kd">const</span> <span class="nx">bestPosition</span> <span class="o">=</span> <span class="nx">matchingResults</span>
  <span class="p">.</span><span class="nx">map</span><span class="p">((</span><span class="nx">result</span><span class="p">)</span> <span class="o">=&gt;</span> <span class="nx">result</span><span class="p">.</span><span class="nx">position</span><span class="p">)</span>
  <span class="p">.</span><span class="nx">filter</span><span class="p">(</span><span class="nb">Number</span><span class="p">.</span><span class="nb">isFinite</span><span class="p">)</span>
  <span class="p">.</span><span class="nx">sort</span><span class="p">((</span><span class="nx">a</span><span class="p">,</span> <span class="nx">b</span><span class="p">)</span> <span class="o">=&gt;</span> <span class="nx">a</span> <span class="o">-</span> <span class="nx">b</span><span class="p">)[</span><span class="mi">0</span><span class="p">];</span>
</code></pre></div></div>

<p>If Google gives us a position result, we’re showing it in the UI. If in case it doesn’t, we just report that the domain was found in the results.</p>

<p>At this point, we have three visibility pointers for the target domain.</p>

<ol>
  <li><strong>Mentioned in answer</strong></li>
  <li><strong>Cited in AI references</strong></li>
  <li><strong>Found in organic results</strong></li>
</ol>

<p>Keeping these pointers separate is important. For instance, a domain can appear in the organic results without being mentioned in the AI answer. It can also be cited by AI Mode without appearing near the top of the organic results. And sometimes, it can be mentioned in the answer without being one of the cited sources.</p>

<p>And so, we want to keep these three pointers separate in the analyzer rather than combining them into a single score as said earlier.</p>

<h2 id="returning-a-stable-application-response">Returning a stable application response</h2>

<p>The application doesn’t send the raw SearchApi responses back to the browser as it is. We’re building and returning a smaller response from the backend so frontend only receives the information it needs to display the results.</p>

<p>For a single query, the response looks something like so.</p>

<div class="language-js highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="p">{</span>
  <span class="nl">query</span><span class="p">:</span> <span class="dl">"</span><span class="s2">best CSS blog</span><span class="dl">"</span><span class="p">,</span>
  <span class="nx">status</span><span class="p">:</span> <span class="dl">"</span><span class="s2">success</span><span class="dl">"</span><span class="p">,</span>
  <span class="nx">aiMode</span><span class="p">:</span> <span class="p">{</span>
    <span class="nl">answerText</span><span class="p">:</span> <span class="dl">"</span><span class="s2">...</span><span class="dl">"</span><span class="p">,</span>
    <span class="nx">mentionedDomain</span><span class="p">:</span> <span class="kc">false</span><span class="p">,</span>
    <span class="nx">references</span><span class="p">:</span> <span class="p">[],</span>
    <span class="nx">otherDomains</span><span class="p">:</span> <span class="p">[</span><span class="dl">"</span><span class="s2">example.com</span><span class="dl">"</span><span class="p">]</span>
  <span class="p">},</span>
  <span class="nx">google</span><span class="p">:</span> <span class="p">{</span>
    <span class="nl">results</span><span class="p">:</span> <span class="p">[],</span>
    <span class="nx">foundDomain</span><span class="p">:</span> <span class="kc">false</span><span class="p">,</span>
    <span class="nx">otherDomains</span><span class="p">:</span> <span class="p">[]</span>
  <span class="p">}</span>
<span class="p">}</span>
</code></pre></div></div>

<p>We also return a summary for the whole run.</p>

<div class="language-js highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="p">{</span>
  <span class="nl">targetDomain</span><span class="p">:</span> <span class="dl">"</span><span class="s2">css-tricks.com</span><span class="dl">"</span><span class="p">,</span>
  <span class="nx">analyzedAt</span><span class="p">:</span> <span class="dl">"</span><span class="s2">2026-09-23T10:00:00.000Z</span><span class="dl">"</span><span class="p">,</span>
  <span class="nx">summary</span><span class="p">:</span> <span class="p">{</span>
    <span class="nl">totalQueries</span><span class="p">:</span> <span class="mi">3</span><span class="p">,</span>
    <span class="nx">successfulQueries</span><span class="p">:</span> <span class="mi">3</span><span class="p">,</span>
    <span class="nx">failedQueries</span><span class="p">:</span> <span class="mi">0</span><span class="p">,</span>
    <span class="nx">answerMentionCount</span><span class="p">:</span> <span class="mi">1</span><span class="p">,</span>
    <span class="nx">aiReferenceCount</span><span class="p">:</span> <span class="mi">1</span><span class="p">,</span>
    <span class="nx">regularResultCount</span><span class="p">:</span> <span class="mi">2</span><span class="p">,</span>
    <span class="nx">referencedDomains</span><span class="p">:</span> <span class="p">[</span><span class="dl">"</span><span class="s2">smashingmagazine.com</span><span class="dl">"</span><span class="p">]</span>
  <span class="p">},</span>
  <span class="nx">queries</span><span class="p">:</span> <span class="p">[]</span>
<span class="p">}</span>
</code></pre></div></div>

<p>We’re creating this succinct response structure so the frontend doesn’t need to know anything about the underlying API responses and the results stay consistent.</p>

<h2 id="running-both-searches-for-a-query">Running both searches for a query</h2>

<p>For each query, we need to make two SearchApi requests: <strong>one to Google AI Mode</strong> and <strong>one to regular Google search</strong>. Since they’re independent of each other, we can run them in parallel like so.</p>

<div class="language-js highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="kd">const</span> <span class="p">[</span><span class="nx">ai</span><span class="p">,</span> <span class="nx">google</span><span class="p">]</span> <span class="o">=</span> <span class="k">await</span> <span class="nb">Promise</span><span class="p">.</span><span class="nx">allSettled</span><span class="p">([</span>
  <span class="nx">searchAiMode</span><span class="p">({</span>
    <span class="nx">query</span><span class="p">,</span>
    <span class="na">apiKey</span><span class="p">:</span> <span class="nx">process</span><span class="p">.</span><span class="nx">env</span><span class="p">.</span><span class="nx">SEARCHAPI_API_KEY</span><span class="p">,</span>
  <span class="p">}),</span>
  <span class="nx">searchGoogle</span><span class="p">({</span>
    <span class="nx">query</span><span class="p">,</span>
    <span class="na">apiKey</span><span class="p">:</span> <span class="nx">process</span><span class="p">.</span><span class="nx">env</span><span class="p">.</span><span class="nx">SEARCHAPI_API_KEY</span><span class="p">,</span>
  <span class="p">}),</span>
<span class="p">]);</span>
</code></pre></div></div>

<p>As you can tell, we’re using <a href="/run-multiple-awaits-in-parallel-using-promise-all/#using-promiseallsettled-method"><code class="language-plaintext highlighter-rouge">Promise.allSettled()</code></a> here instead of <code class="language-plaintext highlighter-rouge">Promise.all()</code> because we don’t want one failed request to hamper the other result. If AI Mode succeeds but regular Google search fails, we can still show the AI Mode result and mark the query as partial. And vice versa.</p>

<p>If both requests fail, we mark the query as an error.</p>

<p>We’re also limiting each run to ten queries. Since each query makes two SearchApi requests, that gives us a predictable maximum of 20 requests per run, keeping the interface reasonably fast.</p>

<h2 id="building-the-form">Building the form</h2>

<p>The frontend for this application is intentionally simple. It only needs a domain, a list of queries, and a button to start the analysis. And That’s it.</p>

<div class="language-html highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="nt">&lt;form</span> <span class="na">id=</span><span class="s">"analyze-form"</span><span class="nt">&gt;</span>
  <span class="nt">&lt;label</span> <span class="na">for=</span><span class="s">"domain"</span><span class="nt">&gt;</span>Domain<span class="nt">&lt;/label&gt;</span>
  <span class="nt">&lt;input</span>
    <span class="na">id=</span><span class="s">"domain"</span>
    <span class="na">name=</span><span class="s">"domain"</span>
    <span class="na">type=</span><span class="s">"text"</span>
    <span class="na">placeholder=</span><span class="s">"example.com"</span>
    <span class="na">required</span>
  <span class="nt">/&gt;</span>

  <span class="nt">&lt;label</span> <span class="na">for=</span><span class="s">"queries"</span><span class="nt">&gt;</span>Queries<span class="nt">&lt;/label&gt;</span>
  <span class="nt">&lt;textarea</span>
    <span class="na">id=</span><span class="s">"queries"</span>
    <span class="na">name=</span><span class="s">"queries"</span>
    <span class="na">rows=</span><span class="s">"6"</span>
    <span class="na">placeholder=</span><span class="s">"best CSS blog
best UI blog"</span>
    <span class="na">required</span>
  <span class="nt">&gt;&lt;/textarea&gt;</span>

  <span class="nt">&lt;button</span> <span class="na">type=</span><span class="s">"submit"</span><span class="nt">&gt;</span>Analyze visibility<span class="nt">&lt;/button&gt;</span>
<span class="nt">&lt;/form&gt;</span>
</code></pre></div></div>

<p>When the form is submitted, the browser sends the input to the backend like so.</p>

<div class="language-js highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="kd">const</span> <span class="nx">response</span> <span class="o">=</span> <span class="k">await</span> <span class="nx">fetch</span><span class="p">(</span><span class="dl">"</span><span class="s2">/api/analyze</span><span class="dl">"</span><span class="p">,</span> <span class="p">{</span>
  <span class="na">method</span><span class="p">:</span> <span class="dl">"</span><span class="s2">POST</span><span class="dl">"</span><span class="p">,</span>
  <span class="na">headers</span><span class="p">:</span> <span class="p">{</span>
    <span class="dl">"</span><span class="s2">content-type</span><span class="dl">"</span><span class="p">:</span> <span class="dl">"</span><span class="s2">application/json</span><span class="dl">"</span><span class="p">,</span>
  <span class="p">},</span>
  <span class="na">body</span><span class="p">:</span> <span class="nx">JSON</span><span class="p">.</span><span class="nx">stringify</span><span class="p">({</span>
    <span class="na">domain</span><span class="p">:</span> <span class="nx">form</span><span class="p">.</span><span class="nx">domain</span><span class="p">.</span><span class="nx">value</span><span class="p">,</span>
    <span class="na">queries</span><span class="p">:</span> <span class="nx">form</span><span class="p">.</span><span class="nx">queries</span><span class="p">.</span><span class="nx">value</span><span class="p">,</span>
  <span class="p">}),</span>
<span class="p">});</span>
</code></pre></div></div>

<p>Once the response comes back, the UI shows a summary followed by a result card for each query. Each card contains the AI answer, its references, the organic results, and the three visibility signals we’ve been tracking.</p>

<p>For the first version, I kept everything visible without introducing a complicated dashboard. If I were to turn this into a larger product, I’d probably collapse long answers and source lists behind expandable sections. You get the idea.</p>

<h2 id="running-the-application">Running the application</h2>

<p>Start the app:</p>

<div class="language-sh highlighter-rouge"><div class="highlight"><pre class="highlight"><code>npm start
</code></pre></div></div>

<p>Then open <a href="http://localhost:3000">http://localhost:3000</a> and enter a domain along with a few queries.</p>

<p>For example…</p>

<div class="language-text highlighter-rouge"><div class="highlight"><pre class="highlight"><code>Domain: css-tricks.com

Queries:
best css blog
css property almanac
css guides
</code></pre></div></div>

<p>Once the analysis finishes, you’ll get the AI Mode answer, its references, the regular Google search results, and the three visibility signals for each query.</p>

<p>One thing to keep in mind is that these are live search results. The exact wording of a query, location, language, timing, and other search conditions can affect what comes back. So, this tool is best thought of as a snapshot of what the search surfaces returned at that time, rather than a permanent ranking report.</p>

<p>Here’s what the input screen looks like.</p>

<p><a href="/images/ai-analyzer-input.png"><img src="/images/ai-analyzer-input.png" alt="AI Analyzer Input" /></a></p>

<p>And here’s the results screen.</p>

<p><a href="/images/ai-analyzer-results.png"><img src="/images/ai-analyzer-results.png" alt="AI Analyzer Results" /></a></p>

<blockquote>
  <p>If you want to try it yourself, the complete source is available on <a href="https://github.com/amitmerchant1990/ai-search-analyzer">GitHub</a>. Fork it, add your SearchApi API key in the <code class="language-plaintext highlighter-rouge">.env</code> file, and you’re ready to go.</p>
</blockquote>

<h2 id="taking-this-further">Taking this further</h2>

<p>We intentionally kept the first version of this little tool small, but there’s definitely room for growth. We could store runs and track changes over time, add scheduled checks and alerts, or compare multiple domains. It could also be extended to support other search engines and AI-powered search tools.</p>

<p>Things like DB persistence, timestamps, query versioning, etc. are a few good candidates for a future article to cover, but for now, I think this is a good starting point to understand how your website is visible in the world of AI-powered search with as little as possible.</p>

<h2 id="in-closing">In closing</h2>

<p>We built this POC to explore what does a website’s visibility look like when search starts generating answers instead of just showing links.</p>

<p>Our little tool gives us a way to look at that question without reducing everything to a single score. For each query, we can see whether a domain was mentioned in the AI answer, cited as a source, or appeared in the regular search results.</p>

<p>And even though it’s a small tool, it makes the difference between traditional search and AI-powered search much easier to see and understand while still being useful.</p>]]></content><author><name>Amit Merchant</name></author><category term="AI" /><summary type="html"><![CDATA[Last month, I built an AI agent that could search the web before answering a question. While working on the article around that project, I got another project idea and that was around the visibility of a website in the world of AI-powered search.]]></summary><media:thumbnail xmlns:media="http://search.yahoo.com/mrss/" url="https://www.amitmerchant.com/cdn/practical-way-to-check-your-ai-search-visibility.png" /><media:content medium="image" url="https://www.amitmerchant.com/cdn/practical-way-to-check-your-ai-search-visibility.png" xmlns:media="http://search.yahoo.com/mrss/" /></entry><entry><title type="html">7 Game-Changing Web UI Features You Can Start Using Today</title><link href="https://www.amitmerchant.com/7-game-changing-web-ui-features-you-need-to-start-using-today/" rel="alternate" type="text/html" title="7 Game-Changing Web UI Features You Can Start Using Today" /><published>2026-10-06T00:00:00+00:00</published><updated>2026-10-06T00:00:00+00:00</updated><id>https://www.amitmerchant.com/7-game-changing-web-ui-features-you-need-to-start-using-today</id><content type="html" xml:base="https://www.amitmerchant.com/7-game-changing-web-ui-features-you-need-to-start-using-today/"><![CDATA[<p>I was watching <a href="https://www.youtube.com/watch?v=uT7MVcCQ4rw">this video</a> from the Google IO on all the new web UI features that are coming to the web platform and I was blown away by how many of these features are already available in modern browsers.</p>

<p>In this article, I want to highlight 7 of these features that I think are game-changing and that you can start using today <em>(of course, in some of the modern browsers)</em>.</p>

<ul id="markdown-toc">
  <li><a href="#css-text-box" id="markdown-toc-css-text-box">CSS Text Box</a></li>
  <li><a href="#css-scrollspy" id="markdown-toc-css-scrollspy">CSS Scrollspy</a></li>
  <li><a href="#contrast-color" id="markdown-toc-contrast-color">Contrast Color</a></li>
  <li><a href="#the-hidey-bar-using-scroll-state-query" id="markdown-toc-the-hidey-bar-using-scroll-state-query">The hidey bar using @scroll-state query</a></li>
  <li><a href="#gap-decorations" id="markdown-toc-gap-decorations">Gap Decorations</a></li>
  <li><a href="#the-light-dark-function" id="markdown-toc-the-light-dark-function">The <code class="language-plaintext highlighter-rouge">light-dark()</code> function</a></li>
  <li><a href="#the-linear-function" id="markdown-toc-the-linear-function">The <code class="language-plaintext highlighter-rouge">linear()</code> function</a></li>
</ul>

<h2 id="css-text-box">CSS Text Box</h2>

<p>A new property called <code class="language-plaintext highlighter-rouge">text-box</code> is coming to the web platforms that allows you to trim extra leading and verical whitespace above and below the text glyphs, enabling precise pixel-perfect alignment and typgraphy layouts without arbitrary negative margins.</p>

<p>The short-hand for the <code class="language-plaintext highlighter-rouge">text-box</code> property:</p>

<div class="language-css highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="nt">text-box</span><span class="o">:</span> <span class="nt">trim-both</span> <span class="nt">cap</span> <span class="nt">alphabetic</span><span class="o">;</span>
</code></pre></div></div>

<p>The long-hand version of the <code class="language-plaintext highlighter-rouge">text-box</code> property:</p>

<div class="language-css highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="nt">text-box-trim</span><span class="o">:</span> <span class="nt">trim-start</span> <span class="o">|</span> <span class="nt">trim-end</span> <span class="o">|</span> <span class="nt">trim-both</span> <span class="o">|</span> <span class="nt">none</span><span class="o">;</span>
<span class="nt">text-box-edge</span><span class="o">:</span> <span class="nt">cap</span> <span class="o">|</span> <span class="nt">ex</span> <span class="o">|</span> <span class="nt">text</span> <span class="o">|</span> <span class="nt">leading</span> <span class="o">+</span> <span class="nt">alphabetic</span> <span class="o">|</span> <span class="nt">text</span><span class="o">;</span>
</code></pre></div></div>

<p>As you can tell, the <code class="language-plaintext highlighter-rouge">text-box</code> property has two parts. The first part is the <code class="language-plaintext highlighter-rouge">text-box-trim</code> property which controls how the text box is trimmed. The second part is the <code class="language-plaintext highlighter-rouge">text-box-edge</code> property which controls which edge of the text box is used for alignment.</p>

<p>This is essentially useful for when you want to align text with other elements on the page and you don’t want to use negative margins or padding to do so.</p>

<p>The following CodePen demonstrates how this looks like in action.</p>

<p class="codepen" data-height="300" data-pen-title="Interactive CSS text-box " data-slug-hash="RNbyooE" data-user="web-dot-dev" style="height: 300px; box-sizing: border-box; display: flex; align-items: center; justify-content: center; border: 2px solid; margin: 1em 0; padding: 1em;">
  <span>See the Pen <a href="https://codepen.io/web-dot-dev/pen/RNbyooE">
  Interactive CSS text-box </a> by web.dev (<a href="https://codepen.io/web-dot-dev">@web-dot-dev</a>)
  on <a href="https://codepen.io">CodePen</a>.</span>
</p>
<script async="" src="https://public.codepenassets.com/embed/index.js"></script>

<p>In real world, this can be useful in scenarios where the extra leading and vertical whitespace above and below the text glyphs can cause misalignment with other elements on the page. Take this for instance.</p>

<p><img src="/images/text-trim.jpg" alt="" /></p>

<p>As you can tell, with the <code class="language-plaintext highlighter-rouge">text-box</code> property, we are able to align the text with the image quite nicely without having to use negative margins or padding.</p>

<p><a href="https://developer.chrome.com/blog/css-text-box-trim">Read more</a></p>

<h2 id="css-scrollspy">CSS Scrollspy</h2>

<p>Another interesting new property called <code class="language-plaintext highlighter-rouge">scroll-target-group</code> can be used to create a scrollspy effect on a page. So, if you don’t know what a scrollspy effect is, it’s a web design UI pattern where navigation links or table-of-contents items automatically update and highlight in real time based on the section of the page currently visible in the user’s viewport.</p>

<p>To implement this, you will need to have a list of navigation links or table-of-contents items that are possibly combined under an element like <code class="language-plaintext highlighter-rouge">&lt;ol&gt;</code> or <code class="language-plaintext highlighter-rouge">&lt;ul&gt;</code>. And these navigation links are tied to the sections of the page that they represent using the <code class="language-plaintext highlighter-rouge">href</code> attribute. The sections of the page are marked with an <code class="language-plaintext highlighter-rouge">id</code> attribute that matches the <code class="language-plaintext highlighter-rouge">href</code> attribute of the navigation links.</p>

<p>Something like this.</p>

<div class="language-html highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="nt">&lt;ol</span> <span class="na">id=</span><span class="s">"toc"</span><span class="nt">&gt;</span>
  <span class="nt">&lt;li&gt;&lt;a</span> <span class="na">href=</span><span class="s">"#section1"</span><span class="nt">&gt;</span>Section 1<span class="nt">&lt;/a&gt;&lt;/li&gt;</span>
  <span class="nt">&lt;li&gt;&lt;a</span> <span class="na">href=</span><span class="s">"#section2"</span><span class="nt">&gt;</span>Section 2<span class="nt">&lt;/a&gt;&lt;/li&gt;</span>
  <span class="nt">&lt;li&gt;&lt;a</span> <span class="na">href=</span><span class="s">"#section3"</span><span class="nt">&gt;</span>Section 3<span class="nt">&lt;/a&gt;&lt;/li&gt;</span>
<span class="nt">&lt;/ol&gt;</span>

<span class="nt">&lt;section</span> <span class="na">id=</span><span class="s">"section1"</span><span class="nt">&gt;</span>
  <span class="nt">&lt;h2&gt;</span>Section 1<span class="nt">&lt;/h2&gt;</span>
  <span class="nt">&lt;p&gt;</span>Content for section 1...<span class="nt">&lt;/p&gt;</span>
<span class="nt">&lt;/section&gt;</span>
<span class="nt">&lt;section</span> <span class="na">id=</span><span class="s">"section2"</span><span class="nt">&gt;</span>
  <span class="nt">&lt;h2&gt;</span>Section 2<span class="nt">&lt;/h2&gt;</span>
  <span class="nt">&lt;p&gt;</span>Content for section 2...<span class="nt">&lt;/p&gt;</span>
<span class="nt">&lt;/section&gt;</span>
<span class="nt">&lt;section</span> <span class="na">id=</span><span class="s">"section3"</span><span class="nt">&gt;</span> 
  <span class="nt">&lt;h2&gt;</span>Section 3<span class="nt">&lt;/h2&gt;</span>
  <span class="nt">&lt;p&gt;</span>Content for section 3...<span class="nt">&lt;/p&gt;</span>
<span class="nt">&lt;/section&gt;</span>
</code></pre></div></div>

<p>Now, we can implement the scrollspy effect using the <code class="language-plaintext highlighter-rouge">scroll-target-group</code> property like so.</p>

<div class="language-css highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="nt">ol</span> <span class="p">{</span>
  <span class="nl">background-color</span><span class="p">:</span> <span class="no">gray</span><span class="p">;</span>
  <span class="nl">right</span><span class="p">:</span> <span class="m">10px</span><span class="p">;</span>
  <span class="nl">top</span><span class="p">:</span> <span class="m">10px</span><span class="p">;</span>
  <span class="nl">position</span><span class="p">:</span> <span class="nb">fixed</span><span class="p">;</span>
  <span class="py">scroll-target-group</span><span class="p">:</span> <span class="nb">auto</span><span class="p">;</span>
<span class="p">}</span>

<span class="nt">a</span><span class="nd">:target-current</span> <span class="p">{</span>
  <span class="nl">color</span><span class="p">:</span> <span class="no">red</span><span class="p">;</span>
<span class="p">}</span>

<span class="nt">section</span> <span class="p">{</span>
  <span class="nl">background</span><span class="p">:</span> <span class="n">lightgray</span><span class="p">;</span>
  <span class="nl">height</span><span class="p">:</span> <span class="m">60vh</span><span class="p">;</span>
  <span class="nl">margin</span><span class="p">:</span> <span class="m">10px</span><span class="p">;</span>
<span class="p">}</span>
</code></pre></div></div>

<p>Here’s how it looks like in action.</p>

<p class="codepen" data-height="300" data-pen-title="CSS Scrollspy" data-version="2" data-slug-hash="PwpOOyg" data-user="amit_merchant" style="height: 300px; box-sizing: border-box; display: flex; align-items: center; justify-content: center; border: 2px solid; margin: 1em 0; padding: 1em;">
  <span>See the Pen <a href="https://codepen.io/editor/amit_merchant/pen/01a11028-a19d-75eb-8594-dce6d593eaf9">
  CSS Scrollspy</a> by Amit Merchant (<a href="https://codepen.io/amit_merchant">@amit_merchant</a>)
  on <a href="https://codepen.io">CodePen</a>.</span>
</p>
<script async="" src="https://public.codepenassets.com/embed/index.js"></script>

<p>This property unlocks a lot of possibilities for creating scrollspy effects without having to rely on JavaScript. You can also use this property to create other interesting effects like highlighting the current section in a table-of-contents, or even creating a progress bar that fills up as the user scrolls through the page.</p>

<p><a href="https://una.im/scroll-target-group/">Read more</a></p>

<h2 id="contrast-color">Contrast Color</h2>

<p>Have you ever been in a situation where you have a background color and you want to set the text color to be readable against that background color and you’re not sure what color to use for your text? Well, the new <code class="language-plaintext highlighter-rouge">contrast-color()</code> function in CSS can help you with that.</p>

<p>The function takes a valid CSS color value as an argument and returns a color that has sufficient contrast against this color. This is useful for ensuring that your text is readable against different background colors.</p>

<div class="language-css highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="nt">color</span><span class="o">:</span> <span class="nt">contrast-color</span><span class="o">(</span><span class="nt">purple</span><span class="o">);</span>
<span class="c">/* Returns: white */</span>

<span class="nt">color</span><span class="o">:</span> <span class="nt">contrast-color</span><span class="o">(</span><span class="nt">yellow</span><span class="o">);</span>
<span class="c">/* Returns: black */</span>

<span class="nt">color</span><span class="o">:</span> <span class="nt">contrast-color</span><span class="o">(</span><span class="nt">var</span><span class="o">(</span><span class="nt">--brand-color</span><span class="o">));</span>
<span class="c">/* Returns: whichever of black or white has higher contrast */</span>
</code></pre></div></div>

<p>Here’s a CodePen to demonstrate how this works.</p>

<p class="codepen" data-height="300" data-pen-title="contrast-color()" data-version="2" data-slug-hash="bNqYYXN" data-user="amit_merchant" style="height: 300px; box-sizing: border-box; display: flex; align-items: center; justify-content: center; border: 2px solid; margin: 1em 0; padding: 1em;">
  <span>See the Pen <a href="https://codepen.io/editor/amit_merchant/pen/01a1103e-b6b6-7b46-ad50-323eef4c17fd">
  contrast-color()</a> by Amit Merchant (<a href="https://codepen.io/amit_merchant">@amit_merchant</a>)
  on <a href="https://codepen.io">CodePen</a>.</span>
</p>
<script async="" src="https://public.codepenassets.com/embed/index.js"></script>

<p>Hover over the boxes to see how the text color changes dynamically based on the background color. The <code class="language-plaintext highlighter-rouge">contrast-color()</code> function ensures that the text is always readable against the background color.</p>

<p><a href="https://una.im/contrast-color">Read more</a></p>

<h2 id="the-hidey-bar-using-scroll-state-query">The hidey bar using @scroll-state query</h2>

<p>A new type of CSS scroll-state media query is coming to the web platform that allows you to create a hidey bar effect on your website. This is useful for creating a navigation bar that hides when the user scrolls down and shows when the user scrolls up.</p>

<p>Here’s how that works.</p>

<div class="language-css highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="nt">html</span> <span class="p">{</span>
  <span class="py">container-type</span><span class="p">:</span> <span class="n">scroll-state</span><span class="p">;</span>
<span class="p">}</span>

<span class="nt">header</span> <span class="p">{</span>
  <span class="nl">position</span><span class="p">:</span> <span class="nb">fixed</span><span class="p">;</span>
  <span class="py">inset</span><span class="p">:</span> <span class="m">0</span> <span class="m">0</span> <span class="nb">auto</span><span class="p">;</span>
  <span class="nl">transition</span><span class="p">:</span> <span class="n">translate</span> <span class="m">0.25s</span><span class="p">;</span>
  <span class="py">translate</span><span class="p">:</span> <span class="m">0</span> <span class="m">0</span><span class="p">;</span>

  <span class="c">/* Slide header up when last having scrolled towards the bottom */</span>
  <span class="err">@container</span> <span class="err">scroll-state(</span><span class="py">scrolled</span><span class="p">:</span> <span class="nb">bottom</span><span class="p">)</span> <span class="err">{</span>
    <span class="n">translate</span><span class="p">:</span> <span class="m">0</span> <span class="m">-100%</span><span class="p">;</span>
  <span class="p">}</span>
<span class="err">}</span>
</code></pre></div></div>

<p>As you can tell, first, we set the <code class="language-plaintext highlighter-rouge">container-type</code> property to <code class="language-plaintext highlighter-rouge">scroll-state</code> on the <code class="language-plaintext highlighter-rouge">html</code> element. This allows us to use the <code class="language-plaintext highlighter-rouge">@container</code> rule to query the scroll state of the container.</p>

<p>Next, we set the <code class="language-plaintext highlighter-rouge">position</code> property of the <code class="language-plaintext highlighter-rouge">header</code> element to <code class="language-plaintext highlighter-rouge">fixed</code> and set the <code class="language-plaintext highlighter-rouge">inset</code> property to <code class="language-plaintext highlighter-rouge">0 0 auto</code>. This positions the header at the top of the viewport.</p>

<p>Then, we set the <code class="language-plaintext highlighter-rouge">transition</code> property to <code class="language-plaintext highlighter-rouge">translate 0.25s</code> to smoothly animate the translation of the header when it hides and shows.</p>

<p>Finally, we use the <code class="language-plaintext highlighter-rouge">@container</code> rule to query the scroll state of the container. When the user scrolls down, we translate the header up by setting the <code class="language-plaintext highlighter-rouge">translate</code> property to <code class="language-plaintext highlighter-rouge">0 -100%</code>. When the user scrolls up, we translate the header back down by setting the <code class="language-plaintext highlighter-rouge">translate</code> property to <code class="language-plaintext highlighter-rouge">0 0</code>.</p>

<p>Here’s a CodePen to demonstrate how this works.</p>

<p class="codepen" data-height="300" data-pen-title="Hidey Bar Demo (Hide on Scroll Down, Show on Scroll Up // Scroll State Queries)" data-default-tab="result" data-slug-hash="qEboVXG" data-user="bramus" style="height: 300px; box-sizing: border-box; display: flex; align-items: center; justify-content: center; border: 2px solid; margin: 1em 0; padding: 1em;">
  <span>See the Pen <a href="https://codepen.io/bramus/pen/qEboVXG">
  Hidey Bar Demo (Hide on Scroll Down, Show on Scroll Up // Scroll State Queries)</a> by Bramus (<a href="https://codepen.io/bramus">@bramus</a>)
  on <a href="https://codepen.io">CodePen</a>.</span>
</p>
<script async="" src="https://public.codepenassets.com/embed/index.js"></script>

<p><a href="https://www.bram.us/2025/10/22/solved-by-css-scroll-state-queries-hide-a-header-when-scrolling-down-show-it-again-when-scrolling-up/">Read more</a></p>

<h2 id="gap-decorations">Gap Decorations</h2>

<p>CSS Gap Decorations allow you to draw visual lines (dividers) between items in a Flexbox or CSS Grid layout using native CSS properties, eliminating the need for extra HTML elements or complex CSS workarounds.</p>

<p>Previously, styling gaps between layout items required workaround solutions, such as adding extra &lt;div&gt; elements, setting outer borders, or applying pseudo-elements (<code class="language-plaintext highlighter-rouge">::before</code> / <code class="language-plaintext highlighter-rouge">::after</code>).</p>

<p>These workarounds had several drawbacks:</p>

<ul>
  <li><strong>Cluttered Markup:</strong> Required adding unnecessary HTML elements solely for visual presentation.</li>
  <li><strong>Accessibility</strong> Issues: Unnecessary decorative elements cluttered screen reader output.</li>
  <li><strong>Maintenance Overhead:</strong> Complex geometric calculations were needed to prevent unwanted border overlaps.</li>
</ul>

<p>With gap decorations, you can directly style gaps in Grid and Flexbox containers:</p>

<ul>
  <li><code class="language-plaintext highlighter-rouge">column-rule</code>: Styles vertical gap lines between columns.</li>
  <li><code class="language-plaintext highlighter-rouge">row-rule</code>: Styles horizontal gap lines between rows.</li>
  <li><code class="language-plaintext highlighter-rouge">rule</code>: A shorthand property that applies styles to both row and column gaps simultaneously.</li>
</ul>

<p>And here’s an example of how to use it.</p>

<div class="language-css highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="nc">.container</span> <span class="p">{</span>
  <span class="nl">display</span><span class="p">:</span> <span class="n">flex</span><span class="p">;</span>
  <span class="nl">column-rule</span><span class="p">:</span> <span class="m">2px</span> <span class="nb">dashed</span> <span class="m">#ccc</span><span class="p">;</span> <span class="c">/* Adds a dashed divider between columns */</span>
  <span class="py">row-rule</span><span class="p">:</span> <span class="m">1px</span> <span class="nb">solid</span> <span class="m">#ddd</span><span class="p">;</span>    <span class="c">/* Adds a solid divider between rows */</span>
<span class="p">}</span>
</code></pre></div></div>

<p>Here’s a simple CodePen to demonstrate how this works.</p>

<p class="codepen" data-height="300" data-pen-title="Gap Decorations" data-version="2" data-default-tab="result" data-slug-hash="ogZoqyq" data-user="amit_merchant" style="height: 300px; box-sizing: border-box; display: flex; align-items: center; justify-content: center; border: 2px solid; margin: 1em 0; padding: 1em;">
  <span>See the Pen <a href="https://codepen.io/editor/amit_merchant/pen/01a1113e-7d86-7300-b73f-7f2a396812b6">
  Gap Decorations</a> by Amit Merchant (<a href="https://codepen.io/amit_merchant">@amit_merchant</a>)
  on <a href="https://codepen.io">CodePen</a>.</span>
</p>
<script async="" src="https://public.codepenassets.com/embed/index.js"></script>

<p><a href="https://developer.chrome.com/blog/gap-decorations-stable">Read more</a></p>

<h2 id="the-light-dark-function">The <code class="language-plaintext highlighter-rouge">light-dark()</code> function</h2>

<p>A really nice addition to the CSS color functions is the <code class="language-plaintext highlighter-rouge">light-dark()</code> function which is also now widely supported across all modern browsers. This function allows you to define a color that adapts to the user’s preferred color scheme (light or dark mode).</p>

<p>For this function to work, you first need to define <code class="language-plaintext highlighter-rouge">color-scheme</code> property on the <code class="language-plaintext highlighter-rouge">html</code> element like so.</p>

<div class="language-css highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="nt">html</span> <span class="p">{</span>
  <span class="py">color-scheme</span><span class="p">:</span> <span class="n">light</span> <span class="n">dark</span><span class="p">;</span>
<span class="p">}</span>
</code></pre></div></div>

<p>What this does is it tells the browser that your website supports both light and dark color schemes. Now, you can use the <code class="language-plaintext highlighter-rouge">light-dark()</code> function to define colors that the browser will automatically switch between based on the user’s preferred color scheme.</p>

<div class="language-css highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="nc">.card</span> <span class="p">{</span>
  <span class="nl">background-color</span><span class="p">:</span> <span class="n">light-dark</span><span class="p">(</span>
    <span class="m">#fff</span><span class="p">,</span> <span class="c">/* Light mode color */</span>
    <span class="m">#000</span>  <span class="c">/* Dark mode color */</span>
  <span class="p">);</span>
  <span class="nl">color</span><span class="p">:</span> <span class="n">light-dark</span><span class="p">(</span><span class="m">#000</span><span class="p">,</span> <span class="m">#fff</span><span class="p">);</span>
<span class="p">}</span>
</code></pre></div></div>

<p>And recently, the <code class="language-plaintext highlighter-rouge">light-dark()</code> function has been updated to also accept images, where instead of defining a color for light and dark mode, you can define an image for light and dark mode.</p>

<div class="language-css highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="nc">.card</span> <span class="p">{</span>
  <span class="nl">background-image</span><span class="p">:</span> <span class="n">light-dark</span><span class="p">(</span>
    <span class="sx">url('light-mode-image.jpg')</span><span class="p">,</span> <span class="c">/* Light mode image */</span>
    <span class="sx">url('dark-mode-image.jpg')</span>  <span class="c">/* Dark mode image */</span>
  <span class="p">);</span>
<span class="p">}</span>
</code></pre></div></div>

<p><a href="https://web.dev/articles/light-dark">Read more</a></p>

<h2 id="the-linear-function">The <code class="language-plaintext highlighter-rouge">linear()</code> function</h2>

<p>If you’ve ever wanted to implement natural, physics-based easing animations (such as bouncy or spring-like effects) without relying on heave JavaScript animation libraries, the new <code class="language-plaintext highlighter-rouge">linear()</code> function in CSS is here to help.</p>

<p>This function allows you to create animations that mimic real-world physics, making your UI feel more dynamic and responsive.</p>

<p>So, traditional CSS easing functions like ease-in-out or standard cubic beziers cannot produce overshoot or spring-like bouncing effects because they are constrained to simple curve control points.</p>

<p>On the other hand, The <code class="language-plaintext highlighter-rouge">linear()</code> function accepts an unlimited series of comma-separated stop values (typically between 0 and 1, though values can overshoot above 1 or go below 0 to create bounces).</p>

<p>By specifying multiple key points along a timeline, developers can approximate realistic physical properties such as mass, stiffness, damping, and velocity to simulate natural motion.</p>

<p>Here’s an example of how to use the <code class="language-plaintext highlighter-rouge">linear()</code> function to create a bouncy animation.</p>

<div class="language-css highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="nc">.dialog</span> <span class="p">{</span>
  <span class="c">/* Approximates a spring/bounce effect using linear() stop points */</span>
  <span class="nl">animation-timing-function</span><span class="p">:</span> <span class="n">linear</span><span class="p">(</span>
    <span class="m">0</span><span class="p">,</span> <span class="m">0.218</span> <span class="m">2.1%</span><span class="p">,</span> <span class="m">0.862</span> <span class="m">6.5%</span><span class="p">,</span> <span class="m">1.114</span> <span class="m">10.2%</span><span class="p">,</span> <span class="m">1.2</span> <span class="m">13%</span><span class="p">,</span> 
    <span class="m">1.187</span> <span class="m">16%</span><span class="p">,</span> <span class="m">1.018</span> <span class="m">24.2%</span><span class="p">,</span> <span class="m">0.98</span> <span class="m">29%</span><span class="p">,</span> <span class="m">1.002</span> <span class="m">42.8%</span><span class="p">,</span> <span class="m">1</span>
  <span class="p">);</span>
<span class="p">}</span>
</code></pre></div></div>

<p>And a little CodePen to demonstrate how this works.</p>

<p class="codepen" data-height="300" data-pen-title="The linear() function" data-version="2" data-default-tab="result" data-slug-hash="NPpwYJP" data-user="amit_merchant" style="height: 300px; box-sizing: border-box; display: flex; align-items: center; justify-content: center; border: 2px solid; margin: 1em 0; padding: 1em;">
  <span>See the Pen <a href="https://codepen.io/editor/amit_merchant/pen/01a1114d-3969-793c-9b67-f7dbbfe250a1">
  The linear() function</a> by Amit Merchant (<a href="https://codepen.io/amit_merchant">@amit_merchant</a>)
  on <a href="https://codepen.io">CodePen</a>.</span>
</p>
<script async="" src="https://public.codepenassets.com/embed/index.js"></script>

<p><a href="https://developer.chrome.com/docs/css-ui/css-linear-easing-function">Read more</a></p>]]></content><author><name>Amit Merchant</name></author><category term="CSS" /><summary type="html"><![CDATA[I was watching this video from the Google IO on all the new web UI features that are coming to the web platform and I was blown away by how many of these features are already available in modern browsers.]]></summary><media:thumbnail xmlns:media="http://search.yahoo.com/mrss/" url="https://www.amitmerchant.com/cdn/7-game-changing-web-ui-features-you-need-to-start-using-today.png" /><media:content medium="image" url="https://www.amitmerchant.com/cdn/7-game-changing-web-ui-features-you-need-to-start-using-today.png" xmlns:media="http://search.yahoo.com/mrss/" /></entry><entry><title type="html">Build an AI Agent with Real‑Time Web Search in JavaScript</title><link href="https://www.amitmerchant.com/building-web-searching-ai-agent-javascript/" rel="alternate" type="text/html" title="Build an AI Agent with Real‑Time Web Search in JavaScript" /><published>2026-08-14T00:00:00+00:00</published><updated>2026-08-14T00:00:00+00:00</updated><id>https://www.amitmerchant.com/building-web-searching-ai-agent-javascript</id><content type="html" xml:base="https://www.amitmerchant.com/building-web-searching-ai-agent-javascript/"><![CDATA[<p>Large language models (<a href="https://en.wikipedia.org/wiki/Large_language_model">LLMs</a>) are great at answering questions, but they have an important limitation: <strong>they don’t inherently have access to what’s happening on the web right now.</strong></p>

<p>Ask an LLM about a programming concept and it can probably answer from its existing knowledge. Ask it about the latest JavaScript frameworks, today’s news, or the current price of a product, and its answer may be outdated.</p>

<p>One way to solve this is to give the model a tool or capability that can search the web before concluding its answer. A form of <strong>Retrieval-Augmented Generation (<a href="https://en.wikipedia.org/wiki/Retrieval-augmented_generation">RAG</a>)</strong> technique.</p>

<p>You might have already seen this pattern in modern AI assistants. For instance, here’s what it looks like in Gemini.</p>

<video autoplay="" loop="" muted="" style="width: 100%; border-radius: 10px;">
  <source src="/images/google-ai-mode-using-web-search.mp4" type="video/mp4" />
  Your browser does not support the video tag.
</video>

<p>In this concise tutorial, we’ll build something similar but slightly bare-bones. A small AI agent in JavaScript that can decide when it needs to search the web, use SearchApi to retrieve Google search results, and then use those results to formulate its answer.</p>

<div class="language-bash highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="nv">$ </span>node agent.js <span class="s2">"What are the latest developments in PHP 8.5?"</span>

🤖 Thinking...

🔎 Searching the web <span class="k">for</span>: <span class="s2">"PHP 8.5 latest developments release"</span>
📄 Found 5 results.

🤖 Thinking...
...
</code></pre></div></div>

<p>As you can tell, if the question requires a web search, the agent will perform the web search and then use the results to answer the question/prompt.</p>

<ul id="markdown-toc">
  <li><a href="#what-makes-something-an-ai-agent" id="markdown-toc-what-makes-something-an-ai-agent">What Makes Something an AI Agent?</a></li>
  <li><a href="#what-were-going-to-build" id="markdown-toc-what-were-going-to-build">What We’re Going to Build</a></li>
  <li><a href="#prerequisites" id="markdown-toc-prerequisites">Prerequisites</a></li>
  <li><a href="#connecting-to-the-llm" id="markdown-toc-connecting-to-the-llm">Connecting to the LLM</a></li>
  <li><a href="#give-the-ai-a-search-tool" id="markdown-toc-give-the-ai-a-search-tool">Give the AI a Search Tool</a></li>
  <li><a href="#implement-search_web-with-searchapi" id="markdown-toc-implement-search_web-with-searchapi">Implement search_web() with SearchApi</a></li>
  <li><a href="#the-agent-loop" id="markdown-toc-the-agent-loop">The Agent Loop</a></li>
  <li><a href="#run-the-agent" id="markdown-toc-run-the-agent">Run the Agent</a></li>
  <li><a href="#why-this-is-an-agent" id="markdown-toc-why-this-is-an-agent">Why This Is an Agent</a></li>
  <li><a href="#taking-the-agent-further" id="markdown-toc-taking-the-agent-further">Taking the Agent Further</a></li>
  <li><a href="#in-closing" id="markdown-toc-in-closing">In Closing</a></li>
</ul>

<h2 id="what-makes-something-an-ai-agent">What Makes Something an AI Agent?</h2>

<p>Before we dive into building our AI agent, let’s quickly understand what makes something an AI agent.</p>

<p>So, a regular LLM would typically take a prompt and generate a response based on its training data. Meaning, the response you get might be outdated or not relevant to the current context and that’s not ideal when you’re looking for up-to-date information.</p>

<div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code>User → LLM → Answer
</code></pre></div></div>

<p>On the other hand, an LLM with a tool can take a prompt, decide if it needs to use a tool (like a web search), use that tool to get real-time information, and then generate a response based on that information. If the tool is not needed, it can just generate a response based on its training data.</p>

<div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code>User → LLM → Tool → Result → LLM → Answer
</code></pre></div></div>

<p>The important distinction here is that the LLM doesn’t execute the tool itself. It decides that a tool is needed and generates a structured tool call. Our application executes the function and sends its result back to the model.</p>

<p>For instance, OpenRouter (the API gateway for LLMs), which we are going to use in this tutorial, exactly describes this as <a href="https://openrouter.ai/docs/guides/features/tool-calling">tool calling</a>. It essentially gives an LLM access to external tools.</p>

<p>And this leads us to the <strong>agent loop</strong>. The agent loop is a process where the LLM can decide to use a tool, get the result, and then decide if it needs to use another tool or generate a final answer. This loop continues until the LLM decides it has enough information to answer the user’s question.</p>

<p><img src="/images/llm-tool-call.png" alt="" /></p>

<p>An agent doesn’t have to be a complicated framework. At its simplest, it’s an LLM, a set of tools, and a loop that allows the model to act on the results.</p>

<p>OpenRouter’s own documentation calls this a <a href="https://openrouter.ai/docs/guides/features/tool-calling#a-simple-agentic-loop">simple agentic loop</a>.</p>

<h2 id="what-were-going-to-build">What We’re Going to Build</h2>

<p>Like I said previously, we’re going to build a small AI agent in JavaScript that can decide when it needs to search the web, use SearchApi to retrieve Google search results, and then use those results to formulate its answer. The flowchart of our agent looks like this.</p>

<p><a href="/images/the-actual-ai-agent.png"><img src="/images/the-actual-ai-agent.png" alt="The flowchart for a web-searching AI agent" /></a></p>

<p>Let’s address all the jargon in the flowchart above so that we understand what all the components do.</p>

<ul>
  <li><strong>Nemotron</strong> — It’s one of OpenRouter’s free LLMs that decides whether it needs to search and what query to use.</li>
  <li><strong>JavaScript</strong> — Orchestrates the agent and executes tools.</li>
  <li><strong>SearchApi</strong> — Retrieves real-time <a href="https://www.searchapi.io/docs/google?utm_source=Dev&amp;utm_medium=Ambassador&amp;utm_campaign=amitmerchant.com">Google search results</a>.</li>
  <li><strong>Agent loop</strong> — Feeds tool results back into the LLM.</li>
</ul>

<p>With that out of the way, let’s get started with building our AI agent.</p>

<h2 id="prerequisites">Prerequisites</h2>

<p>To build this AI agent, you’ll need the following:</p>

<ul>
  <li><strong>Node.js 20+</strong></li>
  <li><strong>An OpenRouter API key</strong></li>
  <li><strong>A SearchApi API key</strong></li>
</ul>

<p>Once you have these, run the following commands to create a new Node.js project and install the required dependencies.</p>

<div class="language-bash highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="nb">mkdir </span>web-search-agent
<span class="nb">cd </span>web-search-agent
npm init <span class="nt">-y</span>
npm <span class="nb">install </span>openai dotenv
</code></pre></div></div>

<p>Now, create a <code class="language-plaintext highlighter-rouge">.env</code> file in the root of your project and add your <strong>OpenRouter</strong> and <strong>SearchApi</strong> API keys like so.</p>

<div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code>OPENROUTER_API_KEY=your_openrouter_api_key
SEARCHAPI_API_KEY=your_searchapi_api_key
</code></pre></div></div>

<p>For this tutorial, I’m using OpenRouter’s <a href="https://openrouter.ai/nvidia/nemotron-3.5-lightning:free">Nemotron 3.5 Lightning</a> model, which is a free model that has the capability to decide when it needs to search the web and what query to use. You can use any other model that has the same capability, but for this tutorial, we’ll stick with Nemotron.</p>

<p>Also, you get a free <a href="https://www.searchapi.io?utm_source=Dev&amp;utm_medium=Ambassador&amp;utm_campaign=amitmerchant.com">SearchApi</a> API key that allows you to make 100 requests, which is more than enough for this tutorial. If you need more, you can upgrade to a paid plan.</p>

<h2 id="connecting-to-the-llm">Connecting to the LLM</h2>

<p>Create a new file called <code class="language-plaintext highlighter-rouge">agent.js</code> in the root of your project and add the following code to connect to the OpenRouter API.</p>

<div class="language-js highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="k">import</span> <span class="dl">"</span><span class="s2">dotenv/config</span><span class="dl">"</span><span class="p">;</span>
<span class="k">import</span> <span class="nx">OpenAI</span> <span class="k">from</span> <span class="dl">"</span><span class="s2">openai</span><span class="dl">"</span><span class="p">;</span>

<span class="kd">const</span> <span class="nx">client</span> <span class="o">=</span> <span class="k">new</span> <span class="nx">OpenAI</span><span class="p">({</span>
  <span class="na">baseURL</span><span class="p">:</span> <span class="dl">"</span><span class="s2">https://openrouter.ai/api/v1</span><span class="dl">"</span><span class="p">,</span>
  <span class="na">apiKey</span><span class="p">:</span> <span class="nx">process</span><span class="p">.</span><span class="nx">env</span><span class="p">.</span><span class="nx">OPENROUTER_API_KEY</span><span class="p">,</span>
<span class="p">});</span>
</code></pre></div></div>

<p>As discussed previously, we’re using OpenRouter’s API to connect to the Nemotron model.</p>

<div class="language-js highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="kd">const</span> <span class="nx">MODEL</span> <span class="o">=</span> <span class="dl">"</span><span class="s2">nvidia/nemotron-3.5-lightning:free</span><span class="dl">"</span><span class="p">;</span>
</code></pre></div></div>

<h2 id="give-the-ai-a-search-tool">Give the AI a Search Tool</h2>

<p>Next, we need to give the AI a tool that it can use to search the web. For this, we need a tool definition.</p>

<div class="language-js highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="kd">const</span> <span class="nx">tools</span> <span class="o">=</span> <span class="p">[</span>
  <span class="p">{</span>
    <span class="na">type</span><span class="p">:</span> <span class="dl">"</span><span class="s2">function</span><span class="dl">"</span><span class="p">,</span>
    <span class="na">function</span><span class="p">:</span> <span class="p">{</span>
      <span class="na">name</span><span class="p">:</span> <span class="dl">"</span><span class="s2">search_web</span><span class="dl">"</span><span class="p">,</span>
      <span class="na">description</span><span class="p">:</span>
        <span class="dl">"</span><span class="s2">Search the web for current information. Use this when the user asks about recent, current, or up-to-date information.</span><span class="dl">"</span><span class="p">,</span>
      <span class="na">parameters</span><span class="p">:</span> <span class="p">{</span>
        <span class="na">type</span><span class="p">:</span> <span class="dl">"</span><span class="s2">object</span><span class="dl">"</span><span class="p">,</span>
        <span class="na">properties</span><span class="p">:</span> <span class="p">{</span>
          <span class="na">query</span><span class="p">:</span> <span class="p">{</span>
            <span class="na">type</span><span class="p">:</span> <span class="dl">"</span><span class="s2">string</span><span class="dl">"</span><span class="p">,</span>
            <span class="na">description</span><span class="p">:</span> <span class="dl">"</span><span class="s2">The search query to send to Google.</span><span class="dl">"</span><span class="p">,</span>
          <span class="p">},</span>
        <span class="p">},</span>
        <span class="na">required</span><span class="p">:</span> <span class="p">[</span><span class="dl">"</span><span class="s2">query</span><span class="dl">"</span><span class="p">],</span>
      <span class="p">},</span>
    <span class="p">},</span>
  <span class="p">},</span>
<span class="p">];</span>
</code></pre></div></div>

<p>Notice, we’re not giving the LLM the implementation of <code class="language-plaintext highlighter-rouge">search_web()</code>. We’re giving it a description of what the function does and the parameters it accepts.</p>

<p>The model can then generate:</p>

<div class="language-json highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="p">{</span><span class="w">
  </span><span class="nl">"type"</span><span class="p">:</span><span class="w"> </span><span class="s2">"function"</span><span class="p">,</span><span class="w">
  </span><span class="nl">"index"</span><span class="p">:</span><span class="w"> </span><span class="mi">0</span><span class="p">,</span><span class="w">
  </span><span class="nl">"id"</span><span class="p">:</span><span class="w"> </span><span class="s2">"call-03f0d876-015c-403e-b2de-3f28973dacc1"</span><span class="p">,</span><span class="w">
  </span><span class="nl">"function"</span><span class="p">:</span><span class="w"> </span><span class="p">{</span><span class="w">
    </span><span class="nl">"name"</span><span class="p">:</span><span class="w"> </span><span class="s2">"search_web"</span><span class="p">,</span><span class="w">
    </span><span class="nl">"arguments"</span><span class="p">:</span><span class="w"> </span><span class="p">{</span><span class="w">
      </span><span class="nl">"query"</span><span class="p">:</span><span class="w"> </span><span class="s2">"latest JavaScript frameworks 2026 trends web development"</span><span class="w">
    </span><span class="p">}</span><span class="w">
  </span><span class="p">}</span><span class="w">
</span><span class="p">}</span><span class="w">
</span></code></pre></div></div>

<p>OpenRouter’s documentation uses the same pattern: <strong>define a function schema</strong>, <strong>receive tool_calls</strong>, <strong>execute the requested function in your application</strong>, and <strong>return the result to the model</strong>.</p>

<h2 id="implement-search_web-with-searchapi">Implement search_web() with SearchApi</h2>

<p>Next, we need to implement the <code class="language-plaintext highlighter-rouge">search_web()</code> function that will use <a href="https://www.searchapi.io?utm_source=Dev&amp;utm_medium=Ambassador&amp;utm_campaign=amitmerchant.com">SearchApi</a> to retrieve Google search results.</p>

<div class="language-js highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="k">async</span> <span class="kd">function</span> <span class="nx">searchWeb</span><span class="p">(</span><span class="nx">query</span><span class="p">)</span> <span class="p">{</span>
  <span class="nx">console</span><span class="p">.</span><span class="nx">log</span><span class="p">(</span><span class="s2">`\n🔎 Searching the web for: "</span><span class="p">${</span><span class="nx">query</span><span class="p">}</span><span class="s2">"`</span><span class="p">);</span>

  <span class="kd">const</span> <span class="nx">url</span> <span class="o">=</span> <span class="k">new</span> <span class="nx">URL</span><span class="p">(</span><span class="dl">"</span><span class="s2">https://www.searchapi.io/api/v1/search</span><span class="dl">"</span><span class="p">);</span>

  <span class="nx">url</span><span class="p">.</span><span class="nx">searchParams</span><span class="p">.</span><span class="kd">set</span><span class="p">(</span><span class="dl">"</span><span class="s2">engine</span><span class="dl">"</span><span class="p">,</span> <span class="dl">"</span><span class="s2">google</span><span class="dl">"</span><span class="p">);</span>
  <span class="nx">url</span><span class="p">.</span><span class="nx">searchParams</span><span class="p">.</span><span class="kd">set</span><span class="p">(</span><span class="dl">"</span><span class="s2">q</span><span class="dl">"</span><span class="p">,</span> <span class="nx">query</span><span class="p">);</span>

  <span class="kd">const</span> <span class="nx">response</span> <span class="o">=</span> <span class="k">await</span> <span class="nx">fetch</span><span class="p">(</span><span class="nx">url</span><span class="p">,</span> <span class="p">{</span>
    <span class="na">headers</span><span class="p">:</span> <span class="p">{</span>
      <span class="na">Authorization</span><span class="p">:</span> <span class="s2">`Bearer </span><span class="p">${</span><span class="nx">process</span><span class="p">.</span><span class="nx">env</span><span class="p">.</span><span class="nx">SEARCHAPI_API_KEY</span><span class="p">}</span><span class="s2">`</span><span class="p">,</span>
    <span class="p">},</span>
  <span class="p">});</span>

  <span class="k">if</span> <span class="p">(</span><span class="o">!</span><span class="nx">response</span><span class="p">.</span><span class="nx">ok</span><span class="p">)</span> <span class="p">{</span>
    <span class="kd">const</span> <span class="nx">error</span> <span class="o">=</span> <span class="k">await</span> <span class="nx">response</span><span class="p">.</span><span class="nx">text</span><span class="p">();</span>

    <span class="k">throw</span> <span class="k">new</span> <span class="nb">Error</span><span class="p">(</span>
      <span class="s2">`SearchApi request failed (</span><span class="p">${</span><span class="nx">response</span><span class="p">.</span><span class="nx">status</span><span class="p">}</span><span class="s2">): </span><span class="p">${</span><span class="nx">error</span><span class="p">}</span><span class="s2">`</span>
    <span class="p">);</span>
  <span class="p">}</span>

  <span class="kd">const</span> <span class="nx">data</span> <span class="o">=</span> <span class="k">await</span> <span class="nx">response</span><span class="p">.</span><span class="nx">json</span><span class="p">();</span>

  <span class="cm">/*
   * We don't need to send the entire SearchApi response
   * back to the LLM. Extract the useful organic results.
   */</span>
  <span class="kd">const</span> <span class="nx">results</span> <span class="o">=</span> <span class="p">(</span><span class="nx">data</span><span class="p">.</span><span class="nx">organic_results</span> <span class="o">||</span> <span class="p">[])</span>
    <span class="p">.</span><span class="nx">slice</span><span class="p">(</span><span class="mi">0</span><span class="p">,</span> <span class="mi">5</span><span class="p">)</span>
    <span class="p">.</span><span class="nx">map</span><span class="p">((</span><span class="nx">result</span><span class="p">)</span> <span class="o">=&gt;</span> <span class="p">({</span>
      <span class="na">title</span><span class="p">:</span> <span class="nx">result</span><span class="p">.</span><span class="nx">title</span><span class="p">,</span>
      <span class="na">link</span><span class="p">:</span> <span class="nx">result</span><span class="p">.</span><span class="nx">link</span><span class="p">,</span>
      <span class="na">snippet</span><span class="p">:</span> <span class="nx">result</span><span class="p">.</span><span class="nx">snippet</span><span class="p">,</span>
    <span class="p">}));</span>

  <span class="nx">console</span><span class="p">.</span><span class="nx">log</span><span class="p">(</span><span class="s2">`📄 Found </span><span class="p">${</span><span class="nx">results</span><span class="p">.</span><span class="nx">length</span><span class="p">}</span><span class="s2"> results.`</span><span class="p">);</span>

  <span class="k">return</span> <span class="nx">results</span><span class="p">;</span>
<span class="p">}</span>
</code></pre></div></div>

<p><strong>Why SearchApi?</strong> An AI agent needs a reliable way to access fresh information from the web. SearchApi provides structured search results through an API, so we can expose web search as a tool without having to build or maintain our own scraping infrastructure. That makes it a natural fit for an agent that needs to ground its answers in current web data.</p>

<h2 id="the-agent-loop">The Agent Loop</h2>

<p>Lastly, we need to build the agent loop that will allow the LLM to decide when it needs to search the web and when it has enough information to answer the user’s question.</p>

<div class="language-js highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="k">async</span> <span class="kd">function</span> <span class="nx">runAgent</span><span class="p">(</span><span class="nx">userQuestion</span><span class="p">)</span> <span class="p">{</span>
  <span class="kd">const</span> <span class="nx">messages</span> <span class="o">=</span> <span class="p">[</span>
    <span class="p">{</span>
      <span class="na">role</span><span class="p">:</span> <span class="dl">"</span><span class="s2">system</span><span class="dl">"</span><span class="p">,</span>
      <span class="na">content</span><span class="p">:</span>
        <span class="dl">"</span><span class="s2">You are a helpful research assistant. Use the search_web tool whenever you need current information. After receiving search results, use them to answer the user's question. Mention the sources you used by including their URLs.</span><span class="dl">"</span><span class="p">,</span>
    <span class="p">},</span>
    <span class="p">{</span>
      <span class="na">role</span><span class="p">:</span> <span class="dl">"</span><span class="s2">user</span><span class="dl">"</span><span class="p">,</span>
      <span class="na">content</span><span class="p">:</span> <span class="nx">userQuestion</span><span class="p">,</span>
    <span class="p">},</span>
  <span class="p">];</span>

  <span class="cm">/*
   * The agent can go through multiple tool calls.
   *
   * For example:
   *
   * LLM → search_web()
   *     → search_web()
   *     → final answer
   */</span>
  <span class="k">for</span> <span class="p">(</span><span class="kd">let</span> <span class="nx">i</span> <span class="o">=</span> <span class="mi">0</span><span class="p">;</span> <span class="nx">i</span> <span class="o">&lt;</span> <span class="mi">5</span><span class="p">;</span> <span class="nx">i</span><span class="o">++</span><span class="p">)</span> <span class="p">{</span>
    <span class="nx">console</span><span class="p">.</span><span class="nx">log</span><span class="p">(</span><span class="s2">`\n🤖 Thinking...`</span><span class="p">);</span>

    <span class="kd">const</span> <span class="nx">response</span> <span class="o">=</span> <span class="k">await</span> <span class="nx">client</span><span class="p">.</span><span class="nx">chat</span><span class="p">.</span><span class="nx">completions</span><span class="p">.</span><span class="nx">create</span><span class="p">({</span>
      <span class="na">model</span><span class="p">:</span> <span class="nx">MODEL</span><span class="p">,</span>
      <span class="nx">messages</span><span class="p">,</span>
      <span class="nx">tools</span><span class="p">,</span>

      <span class="cm">/*
       * We don't need the model's reasoning output for this demo.
       */</span>
      <span class="na">reasoning</span><span class="p">:</span> <span class="p">{</span>
        <span class="na">exclude</span><span class="p">:</span> <span class="kc">true</span><span class="p">,</span>
      <span class="p">},</span>
    <span class="p">});</span>

    <span class="kd">const</span> <span class="nx">message</span> <span class="o">=</span> <span class="nx">response</span><span class="p">.</span><span class="nx">choices</span><span class="p">[</span><span class="mi">0</span><span class="p">].</span><span class="nx">message</span><span class="p">;</span>

    <span class="cm">/*
     * Add the assistant's response to the conversation.
     */</span>
    <span class="nx">messages</span><span class="p">.</span><span class="nx">push</span><span class="p">(</span><span class="nx">message</span><span class="p">);</span>

    <span class="c1">// No tool call means the agent has its final answer.</span>
    <span class="k">if</span> <span class="p">(</span><span class="o">!</span><span class="nx">message</span><span class="p">.</span><span class="nx">tool_calls</span><span class="p">?.</span><span class="nx">length</span><span class="p">)</span> <span class="p">{</span>
      <span class="k">return</span> <span class="nx">message</span><span class="p">.</span><span class="nx">content</span><span class="p">;</span>
    <span class="p">}</span>

    <span class="cm">/*
     * Execute every tool requested by the AI.
     */</span>
    <span class="k">for</span> <span class="p">(</span><span class="kd">const</span> <span class="nx">toolCall</span> <span class="k">of</span> <span class="nx">message</span><span class="p">.</span><span class="nx">tool_calls</span><span class="p">)</span> <span class="p">{</span>
      <span class="k">if</span> <span class="p">(</span><span class="nx">toolCall</span><span class="p">.</span><span class="kd">function</span><span class="p">.</span><span class="nx">name</span> <span class="o">===</span> <span class="dl">"</span><span class="s2">search_web</span><span class="dl">"</span><span class="p">)</span> <span class="p">{</span>
        <span class="kd">const</span> <span class="p">{</span> <span class="nx">query</span> <span class="p">}</span> <span class="o">=</span> <span class="nx">JSON</span><span class="p">.</span><span class="nx">parse</span><span class="p">(</span><span class="nx">toolCall</span><span class="p">.</span><span class="kd">function</span><span class="p">.</span><span class="nx">arguments</span><span class="p">);</span>

        <span class="kd">const</span> <span class="nx">results</span> <span class="o">=</span> <span class="k">await</span> <span class="nx">searchWeb</span><span class="p">(</span><span class="nx">query</span><span class="p">);</span>

        <span class="nx">messages</span><span class="p">.</span><span class="nx">push</span><span class="p">({</span>
          <span class="na">role</span><span class="p">:</span> <span class="dl">"</span><span class="s2">tool</span><span class="dl">"</span><span class="p">,</span>
          <span class="na">tool_call_id</span><span class="p">:</span> <span class="nx">toolCall</span><span class="p">.</span><span class="nx">id</span><span class="p">,</span>
          <span class="na">content</span><span class="p">:</span> <span class="nx">JSON</span><span class="p">.</span><span class="nx">stringify</span><span class="p">(</span><span class="nx">results</span><span class="p">),</span>
        <span class="p">});</span>
      <span class="p">}</span>
    <span class="p">}</span>
  <span class="p">}</span>

  <span class="k">throw</span> <span class="k">new</span> <span class="nb">Error</span><span class="p">(</span><span class="dl">"</span><span class="s2">Agent reached the maximum number of tool calls.</span><span class="dl">"</span><span class="p">);</span>
<span class="p">}</span>
</code></pre></div></div>

<p>As you can tell, the user prompts the agent with a question. The code then checks if the model requested the tool.</p>

<div class="language-js highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="k">if</span> <span class="p">(</span><span class="o">!</span><span class="nx">message</span><span class="p">.</span><span class="nx">tool_calls</span><span class="p">?.</span><span class="nx">length</span><span class="p">)</span> <span class="p">{</span>
  <span class="k">return</span> <span class="nx">message</span><span class="p">.</span><span class="nx">content</span><span class="p">;</span>
<span class="p">}</span>
</code></pre></div></div>

<p>If so, it extracts the arguments from the tool call.</p>

<div class="language-js highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="kd">const</span> <span class="p">{</span> <span class="nx">query</span> <span class="p">}</span> <span class="o">=</span>
  <span class="nx">JSON</span><span class="p">.</span><span class="nx">parse</span><span class="p">(</span><span class="nx">toolCall</span><span class="p">.</span><span class="kd">function</span><span class="p">.</span><span class="nx">arguments</span><span class="p">);</span>
</code></pre></div></div>

<p>Then, it executes the <code class="language-plaintext highlighter-rouge">search_web()</code> function and sends the results back to the model.</p>

<div class="language-js highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="kd">const</span> <span class="nx">results</span> <span class="o">=</span> <span class="k">await</span> <span class="nx">searchWeb</span><span class="p">(</span><span class="nx">query</span><span class="p">);</span>
</code></pre></div></div>

<p>And finally, it adds the tool’s response to the conversation.</p>

<div class="language-js highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="nx">messages</span><span class="p">.</span><span class="nx">push</span><span class="p">({</span>
  <span class="na">role</span><span class="p">:</span> <span class="dl">"</span><span class="s2">tool</span><span class="dl">"</span><span class="p">,</span>
  <span class="na">tool_call_id</span><span class="p">:</span> <span class="nx">toolCall</span><span class="p">.</span><span class="nx">id</span><span class="p">,</span>
  <span class="na">content</span><span class="p">:</span> <span class="nx">JSON</span><span class="p">.</span><span class="nx">stringify</span><span class="p">(</span><span class="nx">results</span><span class="p">),</span>
<span class="p">});</span>
</code></pre></div></div>

<p>Ask the LLM again, and it will either request another tool call or provide its final answer. And that’s how the agent loop works.</p>

<p>This maps almost exactly to OpenRouter’s documented three-step tool-calling flow and its simple agentic loop.</p>

<h2 id="run-the-agent">Run the Agent</h2>

<p>Add the following piece of code to take a question from the command line and run the agent.</p>

<div class="language-js highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="cm">/*
 * Run the agent.
 */</span>

<span class="c1">// eg. "What are the latest JavaScript frameworks for building websites in 2026?"</span>
<span class="kd">const</span> <span class="nx">question</span> <span class="o">=</span> <span class="nx">process</span><span class="p">.</span><span class="nx">argv</span><span class="p">.</span><span class="nx">slice</span><span class="p">(</span><span class="mi">2</span><span class="p">).</span><span class="nx">join</span><span class="p">(</span><span class="dl">"</span><span class="s2"> </span><span class="dl">"</span><span class="p">);</span>

<span class="k">if</span> <span class="p">(</span><span class="o">!</span><span class="nx">question</span><span class="p">)</span> <span class="p">{</span>
  <span class="nx">console</span><span class="p">.</span><span class="nx">error</span><span class="p">(</span><span class="dl">'</span><span class="s1">Usage: node agent.js "your question"</span><span class="dl">'</span><span class="p">);</span>
  <span class="nx">process</span><span class="p">.</span><span class="nx">exit</span><span class="p">(</span><span class="mi">1</span><span class="p">);</span>
<span class="p">}</span>

<span class="nx">console</span><span class="p">.</span><span class="nx">log</span><span class="p">(</span><span class="s2">`\n👤 User: </span><span class="p">${</span><span class="nx">question</span><span class="p">}</span><span class="s2">`</span><span class="p">);</span>

<span class="k">try</span> <span class="p">{</span>
  <span class="kd">const</span> <span class="nx">answer</span> <span class="o">=</span> <span class="k">await</span> <span class="nx">runAgent</span><span class="p">(</span><span class="nx">question</span><span class="p">);</span>

  <span class="nx">console</span><span class="p">.</span><span class="nx">log</span><span class="p">(</span><span class="dl">"</span><span class="se">\n\n</span><span class="s2">💬 Agent:</span><span class="se">\n</span><span class="dl">"</span><span class="p">);</span>
  <span class="nx">console</span><span class="p">.</span><span class="nx">log</span><span class="p">(</span><span class="nx">answer</span><span class="p">);</span>
<span class="p">}</span> <span class="k">catch</span> <span class="p">(</span><span class="nx">error</span><span class="p">)</span> <span class="p">{</span>
  <span class="nx">console</span><span class="p">.</span><span class="nx">error</span><span class="p">(</span><span class="dl">"</span><span class="se">\n</span><span class="s2">❌ Error:</span><span class="dl">"</span><span class="p">);</span>
  <span class="nx">console</span><span class="p">.</span><span class="nx">error</span><span class="p">(</span><span class="nx">error</span><span class="p">);</span>
<span class="p">}</span>
</code></pre></div></div>

<p>Now, you can run the agent with a question like so.</p>

<div class="language-bash highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="nv">$ </span>node agent.js <span class="s2">"Who won the nobel prize in chemistry 2025?"</span>
</code></pre></div></div>

<p>The workflow of the agent will look like this.</p>

<video controls="" autoplay="" style="width: 100%; border-radius: 10px;">
  <source src="/images/web-searching-agent-in-action.mp4" type="video/mp4" />
  Your browser does not support the video tag.
</video>

<p>The same way, you can ask a different question. For instance…</p>

<div class="language-bash highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="nv">$ </span>node agent.js <span class="s2">"What are the best developer laptops available right now?"</span>
</code></pre></div></div>

<p>And the agent will search the web and provide an answer based on the latest information.</p>

<blockquote>
  <p>If you just want to see the project in action, you can check out the <a href="https://github.com/amitmerchant1990/web-search-agent">GitHub repository</a>. Fork it, add your OpenRouter and SearchApi API keys in the <code class="language-plaintext highlighter-rouge">.env</code> file, and you’re good to go.</p>
</blockquote>

<h2 id="why-this-is-an-agent">Why This Is an Agent</h2>

<p>You might be wondering, <em>“Amit! Isn’t this just an LLM calling an API?”</em></p>

<p>Well, no.</p>

<p>The key difference is that the thing we built here can interpret a natural-language request, decide whether it needs external information, select a tool, generate the tool’s arguments, consume the tool’s result, and then produce a final response.</p>

<p>And most importantly, we didn’t tell it when to search or exactly what query to send. We gave it a capability and let the model decide how to use that capability.</p>

<h2 id="taking-the-agent-further">Taking the Agent Further</h2>

<p>As for taking this agent further, you can, for instance, make the agent do multiple searches and combine the results to answer a question.</p>

<p>Let’s say the user asks, <strong>“Compare Astro, Next.js, and Nuxt for building a content-heavy website.”</strong> The agent could search each technology independently and synthesize the results.</p>

<p>You can also utilize SearchApi’s broader API offering that gives you opportunities to add specialized capabilities around things like <a href="https://www.searchapi.io/docs/google-shopping?utm_source=Dev&amp;utm_medium=Ambassador&amp;utm_campaign=amitmerchant.com">shopping</a>, <a href="https://www.searchapi.io/docs/google-maps?utm_source=Dev&amp;utm_medium=Ambassador&amp;utm_campaign=amitmerchant.com">Maps</a>, <a href="https://www.searchapi.io/docs/google-news?utm_source=Dev&amp;utm_medium=Ambassador&amp;utm_campaign=amitmerchant.com">News</a>, <a href="https://www.searchapi.io/docs/youtube?utm_source=Dev&amp;utm_medium=Ambassador&amp;utm_campaign=amitmerchant.com">YouTube</a>, and other search engines/data sources.</p>

<p>So your agent could evolve from:</p>

<div class="language-js highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="nx">search_web</span><span class="p">()</span>
</code></pre></div></div>

<p>into:</p>

<div class="language-js highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="nx">search_web</span><span class="p">()</span>
<span class="nx">search_news</span><span class="p">()</span>
<span class="nx">search_shopping</span><span class="p">()</span>
<span class="nx">search_maps</span><span class="p">()</span>
<span class="nx">search_youtube</span><span class="p">()</span>
</code></pre></div></div>

<p>This would make your agent more capable and versatile, allowing it to answer a wider range of questions with real-time information.</p>

<h2 id="in-closing">In Closing</h2>

<p>We’ve built a small but functional AI agent without an agent framework. The LLM decides when it needs information, our JavaScript application executes the requested tool, SearchApi retrieves the latest search results, and the results are fed back into the model.</p>

<p>The interesting part isn’t the amount of code. It’s the architecture: <strong>an LLM becomes considerably more useful when you give it tools it can decide to use</strong>.</p>

<p>From here, you can add more tools, support multiple searches, introduce memory, or turn the CLI into a full-fledged web application. The possibilities are endless.</p>]]></content><author><name>Amit Merchant</name></author><category term="JavaScript" /><summary type="html"><![CDATA[Large language models (LLMs) are great at answering questions, but they have an important limitation: they don’t inherently have access to what’s happening on the web right now.]]></summary><media:thumbnail xmlns:media="http://search.yahoo.com/mrss/" url="https://www.amitmerchant.com/cdn/building-web-searching-ai-agent-javascript.png" /><media:content medium="image" url="https://www.amitmerchant.com/cdn/building-web-searching-ai-agent-javascript.png" xmlns:media="http://search.yahoo.com/mrss/" /></entry><entry><title type="html">WebMCP in Chrome (A 101 Guide)</title><link href="https://www.amitmerchant.com/webmcp-in-chrome-101/" rel="alternate" type="text/html" title="WebMCP in Chrome (A 101 Guide)" /><published>2026-07-23T00:00:00+00:00</published><updated>2026-07-23T00:00:00+00:00</updated><id>https://www.amitmerchant.com/webmcp-in-chrome-101</id><content type="html" xml:base="https://www.amitmerchant.com/webmcp-in-chrome-101/"><![CDATA[<p><strong>MCP (Model Context Protocol)</strong>. At this point, you might have come across this term if you’ve worked with any kind of AI agent. But if you still haven’t, here’s a quick refresher on what it is and why it’s important.</p>

<p>In simple terms:</p>

<blockquote>
  <p>MCP is like a USB-C port for AI. Instead of building a custom connection for every app or database, MCP gives AI a standard way to connect to external tools.</p>
</blockquote>

<p>For example, imagine an AI assistant that needs to:</p>

<ul>
  <li>Read files from Google Drive</li>
  <li>Check your calendar</li>
  <li>Query a company database</li>
  <li>Use GitHub</li>
  <li>Search a knowledge base</li>
</ul>

<p>With MCP:</p>

<ul>
  <li>The AI knows how to talk using the MCP standard.</li>
  <li>The tool (Google Drive, GitHub, a database, etc.) provides an MCP server.</li>
  <li>They can communicate without needing a unique integration every time.</li>
</ul>

<p>So, essentially, <strong>Model Context Protocol (MCP)</strong> is an open protocol that standardizes how AI models and applications communicate with external tools, data sources, and services by defining a common client-server interface.</p>

<p>Now, let’s talk about <strong>WebMCP</strong>.</p>

<ul id="markdown-toc">
  <li><a href="#what-is-webmcp" id="markdown-toc-what-is-webmcp">What is WebMCP?</a></li>
  <li><a href="#how-to-use-webmcp-in-chrome" id="markdown-toc-how-to-use-webmcp-in-chrome">How to Use WebMCP in Chrome</a></li>
  <li><a href="#registering-tools-via-navigatormodelcontext" id="markdown-toc-registering-tools-via-navigatormodelcontext">Registering Tools via <code class="language-plaintext highlighter-rouge">navigator.modelContext</code></a></li>
  <li><a href="#registering-multiple-tools-or-batching-providecontext" id="markdown-toc-registering-multiple-tools-or-batching-providecontext">Registering Multiple Tools or Batching (<code class="language-plaintext highlighter-rouge">provideContext</code>)</a></li>
  <li><a href="#unregistering-tools" id="markdown-toc-unregistering-tools">Unregistering Tools</a></li>
  <li><a href="#in-closing" id="markdown-toc-in-closing">In closing</a></li>
  <li><a href="#further-reading" id="markdown-toc-further-reading">Further Reading</a></li>
</ul>

<h2 id="what-is-webmcp">What is WebMCP?</h2>

<p>Think of this analogy where a real person is interacting with a website. The person can see the page, click buttons, fill out forms, and navigate through the site. Now, imagine an AI agent doing the same thing. Normally, an AI agent behaves like a person using a browser. It has to inspect the DOM, find buttons, understand layouts, and hope the UI hasn’t changed.</p>

<p>In a nutshell, the AI agents (such as an in-browser agent, Claude Desktop, or Cursor) attempt to do vision-based browsing where they take screenshots of the page and try to click elements visually.</p>

<p>This approach is slow, expensive (vision tokens cost more), and error-prone when handling complex UIs (e.g., hover menus or sliders). And this is where <strong>WebMCP</strong> comes in.</p>

<p>With WebMCP, the website explicitly tells the AI agent, <strong>“Here are the things you can do and the data you can access.”</strong> And so, it doesn’t have to guess or scrape the page to do its thing.</p>

<p>So, a concise definition of WebMCP goes like this.</p>

<blockquote>
  <p><a href="https://github.com/webmachinelearning/webmcp?tab=readme-ov-file">WebMCP</a> is a browser-based implementation of the Model Context Protocol (MCP) that enables web applications to expose standardized tools, resources, and prompts directly to AI agents running in or through the browser, allowing structured interaction without relying on DOM automation or screen scraping.</p>
</blockquote>

<p>WebMCP brings the idea of MCP directly into the browser, allowing web applications to expose MCP capabilities to AI assistants.</p>

<p>In other words:</p>

<p><strong>Web page</strong> → <strong>WebMCP</strong> → <strong>AI Agent</strong></p>

<p>instead of</p>

<p><strong>Web page</strong> → <strong>DOM scraping</strong> → <strong>AI Agent</strong></p>

<p>So, WebMCP is a bridge between web applications and AI agents, allowing them to communicate in a structured way that is more performant and reliable.</p>

<h2 id="how-to-use-webmcp-in-chrome">How to Use WebMCP in Chrome</h2>

<p>WebMCP is supported starting in Chrome 149 as an <a href="https://developer.chrome.com/origintrials/#/view_trial/4163014905550602241">origin trial</a>. You can <strong>enable</strong> support for WebMCP in Chrome by turning on the <strong>WebMCP support in DevTools</strong> and <strong>WebMCP for testing</strong> flags in <code class="language-plaintext highlighter-rouge">chrome://flags</code>.</p>

<p><a href="/images/webmcp-chrom-flags.png"><img src="/images/webmcp-chrom-flags.png" alt="WebMCP Flags in Chrome" /></a></p>

<p>Once done, you can see a new <strong>WebMCP</strong> tab in the Chrome DevTools panel like so.</p>

<p><a href="/images/webmcp-tab-chrome.png"><img src="/images/webmcp-tab-chrome.png" alt="WebMCP Tab in Chrome DevTools" /></a></p>

<p>This tab allows you to inspect and interact with the MCP tools that a web page exposes to AI agents. You can see the registered tools, their input schemas, and even test invoking them directly from the DevTools interface.</p>

<h2 id="registering-tools-via-navigatormodelcontext">Registering Tools via <code class="language-plaintext highlighter-rouge">navigator.modelContext</code></h2>

<p>Now, let’s see how a web page can register tools for AI agents to discover and use. This is done using the <code class="language-plaintext highlighter-rouge">navigator.modelContext</code> API.</p>

<p>So, for instance, if you want to register a tool that allows an AI agent to add a new to-do item on your web app, you can do it like so.</p>

<div class="language-js highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="c1">// Check if WebMCP API is supported in the browser</span>
<span class="k">if</span> <span class="p">(</span><span class="dl">"</span><span class="s2">modelContext</span><span class="dl">"</span> <span class="k">in</span> <span class="nb">navigator</span><span class="p">)</span> <span class="p">{</span>

  <span class="c1">// Register an individual MCP tool</span>
  <span class="nb">navigator</span><span class="p">.</span><span class="nx">modelContext</span><span class="p">.</span><span class="nx">registerTool</span><span class="p">({</span>
    <span class="na">name</span><span class="p">:</span> <span class="dl">"</span><span class="s2">add_todo</span><span class="dl">"</span><span class="p">,</span>
    <span class="na">description</span><span class="p">:</span> <span class="dl">"</span><span class="s2">Adds a new to-do item to the user's task list.</span><span class="dl">"</span><span class="p">,</span>
    <span class="na">inputSchema</span><span class="p">:</span> <span class="p">{</span>
      <span class="na">type</span><span class="p">:</span> <span class="dl">"</span><span class="s2">object</span><span class="dl">"</span><span class="p">,</span>
      <span class="na">properties</span><span class="p">:</span> <span class="p">{</span>
        <span class="na">title</span><span class="p">:</span> <span class="p">{</span>
          <span class="na">type</span><span class="p">:</span> <span class="dl">"</span><span class="s2">string</span><span class="dl">"</span><span class="p">,</span>
          <span class="na">description</span><span class="p">:</span> <span class="dl">"</span><span class="s2">The description or text of the to-do item.</span><span class="dl">"</span>
        <span class="p">}</span>
      <span class="p">},</span>
      <span class="na">required</span><span class="p">:</span> <span class="p">[</span><span class="dl">"</span><span class="s2">title</span><span class="dl">"</span><span class="p">]</span>
    <span class="p">},</span>
    <span class="c1">// Handler function executed when the AI agent calls this tool</span>
    <span class="na">execute</span><span class="p">:</span> <span class="k">async</span> <span class="p">({</span> <span class="nx">title</span> <span class="p">})</span> <span class="o">=&gt;</span> <span class="p">{</span>
      <span class="kd">const</span> <span class="nx">response</span> <span class="o">=</span> <span class="k">await</span> <span class="nx">fetch</span><span class="p">(</span><span class="dl">"</span><span class="s2">/api/todos</span><span class="dl">"</span><span class="p">,</span> <span class="p">{</span>
        <span class="na">method</span><span class="p">:</span> <span class="dl">"</span><span class="s2">POST</span><span class="dl">"</span><span class="p">,</span>
        <span class="na">headers</span><span class="p">:</span> <span class="p">{</span> <span class="dl">"</span><span class="s2">Content-Type</span><span class="dl">"</span><span class="p">:</span> <span class="dl">"</span><span class="s2">application/json</span><span class="dl">"</span> <span class="p">},</span>
        <span class="na">body</span><span class="p">:</span> <span class="nx">JSON</span><span class="p">.</span><span class="nx">stringify</span><span class="p">({</span> <span class="nx">title</span> <span class="p">})</span>
      <span class="p">});</span>
      <span class="kd">const</span> <span class="nx">data</span> <span class="o">=</span> <span class="k">await</span> <span class="nx">response</span><span class="p">.</span><span class="nx">json</span><span class="p">();</span>
      <span class="k">return</span> <span class="p">{</span> <span class="na">success</span><span class="p">:</span> <span class="kc">true</span><span class="p">,</span> <span class="na">result</span><span class="p">:</span> <span class="nx">data</span> <span class="p">};</span>
    <span class="p">}</span>
  <span class="p">});</span>

<span class="p">}</span>
</code></pre></div></div>

<p>As you can see, the above code registers a tool named <code class="language-plaintext highlighter-rouge">add_todo</code> that takes a <code class="language-plaintext highlighter-rouge">title</code> as input and adds a new to-do item to the user’s task list. The <code class="language-plaintext highlighter-rouge">execute</code> function defines how the tool behaves when invoked by an AI agent.</p>

<p>You can specify the input schema for the tool using JSON Schema, which allows AI agents to understand what kind of input is expected. In this case, the tool expects an object with a <code class="language-plaintext highlighter-rouge">title</code> property of type string and that it’s a required field.</p>

<p>Instead of writing a whole <code class="language-plaintext highlighter-rouge">execute</code> function, you can also return a preexisting function or a simple action like showing a toast notification.</p>

<div class="language-js highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="nx">execute</span><span class="p">:</span> <span class="p">({</span> <span class="nx">title</span> <span class="p">})</span> <span class="o">=&gt;</span> <span class="p">{</span>
    <span class="k">return</span> <span class="nx">showToast</span><span class="p">(</span><span class="nx">title</span><span class="p">);</span>
<span class="p">}</span>
</code></pre></div></div>

<p>So, now, when an AI agent discovers this tool, it can invoke it with the required input, and the web page will handle the request accordingly.</p>

<h2 id="registering-multiple-tools-or-batching-providecontext">Registering Multiple Tools or Batching (<code class="language-plaintext highlighter-rouge">provideContext</code>)</h2>

<p>You can also pass a collection of context tools like so.</p>

<div class="language-js highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="c1">// Example batching multiple tools using navigator.modelContext</span>
<span class="nb">navigator</span><span class="p">.</span><span class="nx">modelContext</span><span class="p">.</span><span class="nx">provideContext</span><span class="p">({</span>
  <span class="na">tools</span><span class="p">:</span> <span class="p">[</span>
    <span class="p">{</span>
      <span class="na">name</span><span class="p">:</span> <span class="dl">"</span><span class="s2">list_todos</span><span class="dl">"</span><span class="p">,</span>
      <span class="na">description</span><span class="p">:</span> <span class="dl">"</span><span class="s2">Retrieves all current to-dos for the user.</span><span class="dl">"</span><span class="p">,</span>
      <span class="na">inputSchema</span><span class="p">:</span> <span class="p">{</span> <span class="na">type</span><span class="p">:</span> <span class="dl">"</span><span class="s2">object</span><span class="dl">"</span><span class="p">,</span> <span class="na">properties</span><span class="p">:</span> <span class="p">{}</span> <span class="p">},</span>
      <span class="na">execute</span><span class="p">:</span> <span class="k">async</span> <span class="p">()</span> <span class="o">=&gt;</span> <span class="p">{</span>
        <span class="kd">const</span> <span class="nx">res</span> <span class="o">=</span> <span class="k">await</span> <span class="nx">fetch</span><span class="p">(</span><span class="dl">"</span><span class="s2">/api/todos</span><span class="dl">"</span><span class="p">);</span>
        <span class="k">return</span> <span class="k">await</span> <span class="nx">res</span><span class="p">.</span><span class="nx">json</span><span class="p">();</span>
      <span class="p">}</span>
    <span class="p">},</span>
    <span class="p">{</span>
      <span class="na">name</span><span class="p">:</span> <span class="dl">"</span><span class="s2">toggle_todo</span><span class="dl">"</span><span class="p">,</span>
      <span class="na">description</span><span class="p">:</span> <span class="dl">"</span><span class="s2">Toggles a to-do item as completed or incomplete.</span><span class="dl">"</span><span class="p">,</span>
      <span class="na">inputSchema</span><span class="p">:</span> <span class="p">{</span>
        <span class="na">type</span><span class="p">:</span> <span class="dl">"</span><span class="s2">object</span><span class="dl">"</span><span class="p">,</span>
        <span class="na">properties</span><span class="p">:</span> <span class="p">{</span>
          <span class="na">id</span><span class="p">:</span> <span class="p">{</span> <span class="na">type</span><span class="p">:</span> <span class="dl">"</span><span class="s2">string</span><span class="dl">"</span><span class="p">,</span> <span class="na">description</span><span class="p">:</span> <span class="dl">"</span><span class="s2">The ID of the to-do item</span><span class="dl">"</span> <span class="p">}</span>
        <span class="p">},</span>
        <span class="na">required</span><span class="p">:</span> <span class="p">[</span><span class="dl">"</span><span class="s2">id</span><span class="dl">"</span><span class="p">]</span>
      <span class="p">},</span>
      <span class="na">execute</span><span class="p">:</span> <span class="k">async</span> <span class="p">({</span> <span class="nx">id</span> <span class="p">})</span> <span class="o">=&gt;</span> <span class="p">{</span>
        <span class="kd">const</span> <span class="nx">res</span> <span class="o">=</span> <span class="k">await</span> <span class="nx">fetch</span><span class="p">(</span><span class="s2">`/api/todos/</span><span class="p">${</span><span class="nx">id</span><span class="p">}</span><span class="s2">/toggle`</span><span class="p">,</span> <span class="p">{</span> <span class="na">method</span><span class="p">:</span> <span class="dl">"</span><span class="s2">POST</span><span class="dl">"</span> <span class="p">});</span>
        <span class="k">return</span> <span class="k">await</span> <span class="nx">res</span><span class="p">.</span><span class="nx">json</span><span class="p">();</span>
      <span class="p">}</span>
    <span class="p">}</span>
  <span class="p">]</span>
<span class="p">});</span>
</code></pre></div></div>

<p>All the exposed tools can be viewed in the <strong>WebMCP</strong> tab in Chrome DevTools, and AI agents can discover and invoke them as needed.</p>

<p><a href="/images/exposed-webmcp-tools.png"><img src="/images/exposed-webmcp-tools.png" alt="Exposed WebMCP tools" /></a></p>

<p>As you can tell, you can tweak the parameters of the tools and run the tool directly from the DevTools interface to see how it behaves. All the activity for these tools will be logged in the Tool Activity section of the WebMCP tab once an AI agent invokes them.</p>

<p>Here’s what it looks like in action.</p>

<video controls="" autoplay="" style="width: 100%;">
  <source src="/images/webmcp-tool-in-action.mp4" type="video/mp4" />
  Your browser does not support the video tag.
</video>

<h2 id="unregistering-tools">Unregistering Tools</h2>

<p>When a tool should no longer be available to the agent (e.g., when navigating away or changing user state), you can unregister it.</p>

<div class="language-js highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="c1">// Unregistering a specific MCP tool by name</span>
<span class="nb">navigator</span><span class="p">.</span><span class="nx">modelContext</span><span class="p">.</span><span class="nx">unregisterTool</span><span class="p">(</span><span class="dl">"</span><span class="s2">show_toast</span><span class="dl">"</span><span class="p">);</span>
</code></pre></div></div>

<h2 id="in-closing">In closing</h2>

<p>WebMCP marks an important step toward an AI-native web by enabling web applications to expose their capabilities through a standardized interface instead of relying on UI automation or HTML parsing. This makes AI interactions more reliable, efficient, and resilient to interface changes.</p>

<p>Its potential spans a wide range of applications—from note-taking and project management tools to e-commerce, travel, banking, and content platforms. As AI assistants become a natural part of our daily workflows, WebMCP could become the standard way websites communicate with them, much like REST APIs did for traditional applications.</p>

<h2 id="further-reading">Further Reading</h2>

<ul>
  <li><a href="https://developer.chrome.com/docs/ai/webmcp">WebMCP — AI on Chrome</a></li>
  <li><a href="https://github.com/webmachinelearning/webmcp">Official WebMCP GitHub Repository</a></li>
  <li><a href="https://www.youtube.com/watch?v=6wFOUBr0CPE">WebMCP is AWESOME! by Mehul Mohan</a></li>
</ul>]]></content><author><name>Amit Merchant</name></author><category term="AI" /><summary type="html"><![CDATA[MCP (Model Context Protocol). At this point, you might have come across this term if you’ve worked with any kind of AI agent. But if you still haven’t, here’s a quick refresher on what it is and why it’s important.]]></summary><media:thumbnail xmlns:media="http://search.yahoo.com/mrss/" url="https://www.amitmerchant.com/cdn/webmcp-in-chrome-101-guide.png" /><media:content medium="image" url="https://www.amitmerchant.com/cdn/webmcp-in-chrome-101-guide.png" xmlns:media="http://search.yahoo.com/mrss/" /></entry><entry><title type="html">From Feature Branches to Agent Branches ft. Antigravity</title><link href="https://www.amitmerchant.com/from-feature-branches-to-agent-branches-featuring-antigravity/" rel="alternate" type="text/html" title="From Feature Branches to Agent Branches ft. Antigravity" /><published>2026-06-20T00:00:00+00:00</published><updated>2026-06-20T00:00:00+00:00</updated><id>https://www.amitmerchant.com/from-feature-branches-to-agent-branches-featuring-antigravity</id><content type="html" xml:base="https://www.amitmerchant.com/from-feature-branches-to-agent-branches-featuring-antigravity/"><![CDATA[<p>When people talk about AI coding tools, the conversation usually revolves around models, prompts, context windows, and agent capabilities. That’s usually what grabs the headlines.</p>

<p>Yet the thing that caught my attention while using <a href="https://antigravity.google">Antigravity</a> wasn’t the AI at all. It was something completely unrelated to the recent AI boom and, in software development terms, almost dinosaur-age old.</p>

<ul id="markdown-toc">
  <li><a href="#rediscovering-worktrees" id="markdown-toc-rediscovering-worktrees">Rediscovering Worktrees</a></li>
  <li><a href="#agent-branches-in-action" id="markdown-toc-agent-branches-in-action">Agent Branches in Action</a></li>
  <li><a href="#under-the-hood" id="markdown-toc-under-the-hood">Under the Hood</a></li>
  <li><a href="#in-closing" id="markdown-toc-in-closing">In Closing</a></li>
</ul>

<h2 id="rediscovering-worktrees">Rediscovering Worktrees</h2>

<p>Git worktrees aren’t new to me. In fact, I made <a href="https://www.youtube.com/watch?v=Ziz-AeP8KhY">a video</a> about them back in 2021, calling them a better alternative to <a href="https://amitmerchant.com/store-it-like-git-stash/">Git Stash</a>. At the time, I viewed them as a handy Git feature for juggling multiple pieces of work without constantly stashing and unstashing changes. Useful, certainly, but not something I reached for every day.</p>

<p>That perspective started to change when I noticed Antigravity exposing worktrees directly in its interface. Instead of burying them behind terminal commands, it treated them as a first-class workflow. I decided to lean into that workflow while building a few small features, creating separate worktrees for each one.</p>

<p>What followed wasn’t a lesson in how Git worktrees work since I already knew that part. Instead, it was a realization that worktrees solve a much bigger problem than I had originally given them credit for. Antigravity made me see them not as a convenience feature, but as a compelling way to organize and isolate development work in an AI-assisted world.</p>

<blockquote>
  <p>So, those of you who aren’t familiar with Git worktrees, <strong>they allow you to have multiple working directories associated with a single Git repository</strong>. Each worktree can be on a different branch, and you can switch between them without affecting the others. This means you can have multiple branches checked out at the same time, each in its own directory.</p>
</blockquote>

<h2 id="agent-branches-in-action">Agent Branches in Action</h2>

<p>Antigravity uses this feature to let you create <strong>“agent branches”</strong> for each task or feature you’re working on.</p>

<p>Instead of having a single <strong>“main”</strong> branch where all your work happens, you can have multiple branches that are isolated from each other.</p>

<p>This is especially useful when you’re working on multiple features or tasks simultaneously, as it prevents changes from one branch from affecting the others.</p>

<p>Here’s what it looks like in Antigravity.</p>

<p><a href="/images/antigravity-worktree-1.png"><img src="/images/antigravity-worktree-1.png" alt="Antigravity Worktrees" /></a></p>

<p>As you can tell, Antigravity makes it easy to create a new worktree or continue working from a previous one. Every feature gets its own workspace, making context switching feel much more natural.</p>

<p>The best part about it is that you can easily switch between these worktree branches without worrying about saving or stashing changes right from the Antigravity interface!</p>

<h2 id="under-the-hood">Under the Hood</h2>

<p>Now, if you run <code class="language-plaintext highlighter-rouge">git status</code> in the terminal, here’s what it looks like.</p>

<p><a href="/images/antigravity-worktree-2.png"><img src="/images/antigravity-worktree-2.png" alt="Git Status" /></a></p>

<p>As you can tell, when you run <code class="language-plaintext highlighter-rouge">git status</code>, it shows all the branches created under the worktree, and when you try to check out one of these branches, it won’t let you do that because the branch is already checked out in another worktree (which resides under a namespace under the <code class="language-plaintext highlighter-rouge">.gemini</code> folder, usually in your computer’s home directory).</p>

<div class="language-markdown highlighter-rouge"><div class="highlight"><pre class="highlight"><code>Main Workspace
└── main

Worktree #1
└── add-score-tracking-game

Worktree #2
└── simple-3x3-sudoku-game
</code></pre></div></div>

<p>This means each task gets:</p>

<ul>
  <li>a dedicated branch</li>
  <li>a dedicated checkout</li>
  <li>a dedicated filesystem</li>
</ul>

<p>which is exactly what enables concurrent agent execution.</p>

<blockquote>
  <p>Quick tip: You can list all the worktrees in your repository by running <code class="language-plaintext highlighter-rouge">git worktree list</code>. This will show you all the worktrees that are currently checked out, along with their paths and the branches they are on.</p>
</blockquote>

<p>If, for some reason, you want to manually make changes in one of these branches, you need to switch to that worktree directory first and then make your changes. And once you’re done, you commit from these branches, or merge them back into the main branch just like you would with any other Git workflow.</p>

<h2 id="in-closing">In Closing</h2>

<p>What I like most about this approach is that it makes context switching almost effortless. Instead of juggling branches, stashing changes, or maintaining multiple clones of the same repository, each feature lives in its own isolated workspace. The result is a workflow that feels cleaner, safer, and easier to reason about, especially when multiple streams of work are happening in parallel.</p>

<p>Git worktrees aren’t new, but Antigravity gave me a fresh perspective on them. Rather than treating them as an obscure Git feature reserved for edge cases, it elevates them into a core part of the development experience. And after spending some time with this workflow, I’m starting to think that worktrees may be one of Git’s most underrated features.</p>]]></content><author><name>Amit Merchant</name></author><category term="AI" /><summary type="html"><![CDATA[When people talk about AI coding tools, the conversation usually revolves around models, prompts, context windows, and agent capabilities. That’s usually what grabs the headlines.]]></summary><media:thumbnail xmlns:media="http://search.yahoo.com/mrss/" url="https://www.amitmerchant.com/cdn/from-feature-branches-to-agent-branches-featuring-antigravity.png" /><media:content medium="image" url="https://www.amitmerchant.com/cdn/from-feature-branches-to-agent-branches-featuring-antigravity.png" xmlns:media="http://search.yahoo.com/mrss/" /></entry><entry><title type="html">One CSS Property That Makes Numbers Look Instantly Better</title><link href="https://www.amitmerchant.com/one-css-property-that-makes-numbers-look-instantly-better/" rel="alternate" type="text/html" title="One CSS Property That Makes Numbers Look Instantly Better" /><published>2026-03-05T00:00:00+00:00</published><updated>2026-03-05T00:00:00+00:00</updated><id>https://www.amitmerchant.com/one-css-property-that-makes-numbers-look-instantly-better</id><content type="html" xml:base="https://www.amitmerchant.com/one-css-property-that-makes-numbers-look-instantly-better/"><![CDATA[<p>When you’re working with fonts, especially for displaying numbers, there’s something called <strong>proportional spacing</strong> that can make a huge difference in how your numbers look when they’re animated.</p>

<p>Essentially, most fonts (apart from <a href="https://en.wikipedia.org/wiki/Monospaced_font">monospaced fonts</a>) use <a href="https://www.webopedia.com/definitions/proportional-spacing/">proportional spacing</a>, which means that each character takes up a different amount of horizontal space. This can lead to numbers looking <strong>uneven</strong> and <strong>misaligned</strong>, especially when they’re displayed in a tabular format.</p>

<p>Here’s what it looks like.</p>

<p><img src="/images/proportional-pitch-vs-fixed-pitch.png" alt="proportional vs fixed pitch" /></p>

<p>This creates issues when numbers change, let’s say in clocks or counters, the text/numbers <em>shifts horizontally</em>, which can be visually jarring. In other words, it will cause a layout shift, which is not a good user experience.</p>

<blockquote>
  <p>To fix this issue, you can use the <code class="language-plaintext highlighter-rouge">font-variant-numeric</code> CSS property (which I <a href="https://x.com/sorenblank/status/2028520200417706017">recently stumbled upon</a>) with the value <code class="language-plaintext highlighter-rouge">tabular-nums</code>.</p>
</blockquote>

<p>This will force all the numbers in the font to take up the same amount of horizontal space, creating a more uniform and visually appealing look. Once this property is applied, the numbers behave like monospaced digits, even in proportional fonts.</p>

<p>I’ve created a <a href="https://codepen.io/amit_merchant/pen/dPpMNVd">CodePen demo</a> to show you how it works.</p>

<p class="codepen" data-height="300" data-pen-title="Tabular Numbers Demo" data-slug-hash="dPpMNVd" data-user="amit_merchant" style="height: 300px; box-sizing: border-box; display: flex; align-items: center; justify-content: center; border: 2px solid; margin: 1em 0; padding: 1em;">
  <span>See the Pen <a href="https://codepen.io/amit_merchant/pen/dPpMNVd">
  Tabular Numbers Demo</a> by Amit Merchant (<a href="https://codepen.io/amit_merchant">@amit_merchant</a>)
  on <a href="https://codepen.io">CodePen</a>.</span>
</p>
<script async="" src="https://public.codepenassets.com/embed/index.js"></script>

<p>Now, you might be wondering, what are all the places where this can be useful?</p>

<p>Well, here are a few examples:</p>

<ul>
  <li>Clocks and timers</li>
  <li>Financial tables</li>
  <li>Analytics dashboards</li>
  <li>Scoreboards</li>
  <li>Any place where numbers are displayed in a tabular format</li>
</ul>

<p>Also, this property is supported in all modern browsers, so you can safely use it in your projects without worrying about compatibility issues.</p>

<p>Read more about the <code class="language-plaintext highlighter-rouge">font-variant-numeric</code> property on <a href="https://developer.mozilla.org/en-US/docs/Web/CSS/Reference/Properties/font-variant-numeric">MDN Web Docs</a>.</p>]]></content><author><name>Amit Merchant</name></author><category term="CSS" /><summary type="html"><![CDATA[When you’re working with fonts, especially for displaying numbers, there’s something called proportional spacing that can make a huge difference in how your numbers look when they’re animated.]]></summary><media:thumbnail xmlns:media="http://search.yahoo.com/mrss/" url="https://www.amitmerchant.com/cdn/one-css-property-that-makes-numbers-look-instantly-better.png" /><media:content medium="image" url="https://www.amitmerchant.com/cdn/one-css-property-that-makes-numbers-look-instantly-better.png" xmlns:media="http://search.yahoo.com/mrss/" /></entry><entry><title type="html">Real-world examples of using Laravel AI SDK</title><link href="https://www.amitmerchant.com/real-world-exmples-using-laravel-api-sdk/" rel="alternate" type="text/html" title="Real-world examples of using Laravel AI SDK" /><published>2026-02-07T00:00:00+00:00</published><updated>2026-02-07T00:00:00+00:00</updated><id>https://www.amitmerchant.com/real-world-exmples-using-laravel-api-sdk</id><content type="html" xml:base="https://www.amitmerchant.com/real-world-exmples-using-laravel-api-sdk/"><![CDATA[<p>The Laravel team recently released the <a href="https://github.com/laravel/ai">Laravel AI SDK</a> that provides a simple and elegant way to integrate AI capabilities into your Laravel applications. The SDK offers a clean and intuitive API that allows developers to easily interact with AI models and services.</p>

<p>Essentially, the Laravel AI SDK abstracts away the complexities of working with AI models (such as <strong>OpenAI</strong>, <strong>Anthropic</strong>, <strong>Gemini</strong>, <strong>Mistral</strong>, <strong>Ollama</strong>, and more) and provides a seamless experience for developers to leverage AI in their applications. It supports various AI services, including natural language processing, image recognition, and more.</p>

<p>You can check <a href="https://laravel.com/docs/12.x/ai-sdk">this page</a> to learn how to get started with the Laravel AI SDK and explore its features. In this article, though, I want to share some real-world examples of how you can use the Laravel AI SDK in your applications.</p>

<ul id="markdown-toc">
  <li><a href="#mining-user-data-for-insights" id="markdown-toc-mining-user-data-for-insights">Mining user data for insights</a></li>
  <li><a href="#code-review-bot-for-prs-github--gitlab-integration" id="markdown-toc-code-review-bot-for-prs-github--gitlab-integration">Code Review Bot for PRs (GitHub / GitLab Integration)</a></li>
  <li><a href="#personalized-learning--quiz-system-for-an-edtech-app" id="markdown-toc-personalized-learning--quiz-system-for-an-edtech-app">Personalized Learning / Quiz System for an EdTech App</a></li>
  <li><a href="#in-closing" id="markdown-toc-in-closing">In closing</a></li>
</ul>

<h3 id="mining-user-data-for-insights">Mining user data for insights</h3>

<p>The simplest use case of the Laravel AI SDK is to mine user data for insights. For instance, you can use the SDK to analyze user models and related data to extract valuable insights.</p>

<p>For instance, you can use the <code class="language-plaintext highlighter-rouge">User</code> + <code class="language-plaintext highlighter-rouge">Order</code> Eloquent models as context for your AI queries.</p>

<p>Here’s an <code class="language-plaintext highlighter-rouge">AccountAssistant</code> assistant class that uses the Laravel AI SDK to analyze user data and provide insights:</p>

<div class="language-php highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="c1">// app/Ai/Agents/AccountAssistant.php</span>

<span class="kn">namespace</span> <span class="nn">App\Ai\Agents</span><span class="p">;</span>

<span class="kn">use</span> <span class="nc">App\Models\User</span><span class="p">;</span>
<span class="kn">use</span> <span class="nc">App\Models\Order</span><span class="p">;</span>
<span class="kn">use</span> <span class="nc">Laravel\Ai\Contracts\Agent</span><span class="p">;</span>
<span class="kn">use</span> <span class="nc">Laravel\Ai\Contracts\Conversational</span><span class="p">;</span>
<span class="kn">use</span> <span class="nc">Laravel\Ai\Promptable</span><span class="p">;</span>
<span class="kn">use</span> <span class="nc">Stringable</span><span class="p">;</span>

<span class="kd">class</span> <span class="nc">AccountAssistant</span> <span class="kd">implements</span> <span class="nc">Agent</span><span class="p">,</span> <span class="nc">Conversational</span>
<span class="p">{</span>
    <span class="kn">use</span> <span class="nc">Promptable</span><span class="p">;</span>

    <span class="k">public</span> <span class="k">function</span> <span class="n">__construct</span><span class="p">(</span><span class="k">public</span> <span class="kt">User</span> <span class="nv">$user</span><span class="p">)</span> <span class="p">{}</span>

    <span class="k">public</span> <span class="k">function</span> <span class="n">instructions</span><span class="p">():</span> <span class="kt">Stringable</span><span class="o">|</span><span class="n">string</span>
    <span class="p">{</span>
        <span class="k">return</span> <span class="s1">'You are an assistant helping a customer with their account and recent orders.'</span><span class="p">;</span>
    <span class="p">}</span>

    <span class="k">public</span> <span class="k">function</span> <span class="n">messages</span><span class="p">():</span> <span class="kt">iterable</span>
    <span class="p">{</span>
        <span class="c1">// You might combine past chat messages from a History model,</span>
        <span class="c1">// but you can also “prime” the conversation with DB data:</span>
        <span class="nv">$orders</span> <span class="o">=</span> <span class="nc">Order</span><span class="o">::</span><span class="nf">where</span><span class="p">(</span><span class="s1">'user_id'</span><span class="p">,</span> <span class="nv">$this</span><span class="o">-&gt;</span><span class="n">user</span><span class="o">-&gt;</span><span class="n">id</span><span class="p">)</span>
            <span class="o">-&gt;</span><span class="nf">latest</span><span class="p">()</span>
            <span class="o">-&gt;</span><span class="nf">limit</span><span class="p">(</span><span class="mi">5</span><span class="p">)</span>
            <span class="o">-&gt;</span><span class="nf">get</span><span class="p">()</span>
            <span class="o">-&gt;</span><span class="nf">map</span><span class="p">(</span><span class="k">fn</span> <span class="p">(</span><span class="nv">$order</span><span class="p">)</span> <span class="o">=&gt;</span> <span class="s2">"- #</span><span class="si">{</span><span class="nv">$order</span><span class="o">-&gt;</span><span class="n">id</span><span class="si">}</span><span class="s2"> (</span><span class="si">{</span><span class="nv">$order</span><span class="o">-&gt;</span><span class="n">status</span><span class="si">}</span><span class="s2">) total </span><span class="si">{</span><span class="nv">$order</span><span class="o">-&gt;</span><span class="n">total</span><span class="si">}</span><span class="s2">"</span><span class="p">);</span>

        <span class="k">return</span> <span class="p">[</span>
            <span class="c1">// System / context message built from Eloquent:</span>
            <span class="k">new</span> <span class="err">\</span><span class="nf">Laravel\Ai\Messages\Message</span><span class="p">(</span>
                <span class="s1">'system'</span><span class="p">,</span>
                <span class="s2">"Here are the user's 5 most recent orders:</span><span class="se">\n</span><span class="s2">"</span><span class="mf">.</span><span class="nv">$orders</span><span class="o">-&gt;</span><span class="nb">implode</span><span class="p">(</span><span class="s2">"</span><span class="se">\n</span><span class="s2">"</span><span class="p">)</span>
            <span class="p">),</span>
        <span class="p">];</span>
    <span class="p">}</span>
<span class="p">}</span>
</code></pre></div></div>

<p>And then use it like this:</p>

<div class="language-php highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="nv">$assistant</span> <span class="o">=</span> <span class="err">\</span><span class="nc">App\Ai\Agents\AccountAssistant</span><span class="o">::</span><span class="nf">make</span><span class="p">(</span><span class="n">user</span><span class="o">:</span> <span class="nv">$user</span><span class="p">);</span>

<span class="nv">$response</span> <span class="o">=</span> <span class="nv">$assistant</span><span class="o">-&gt;</span><span class="nf">prompt</span><span class="p">(</span><span class="s1">'Where is my last order?'</span><span class="p">);</span>

<span class="k">return</span> <span class="p">(</span><span class="n">string</span><span class="p">)</span> <span class="nv">$response</span><span class="p">;</span>
</code></pre></div></div>

<p>In this example, the <code class="language-plaintext highlighter-rouge">AccountAssistant</code> class implements the <code class="language-plaintext highlighter-rouge">Agent</code> and <code class="language-plaintext highlighter-rouge">Conversational</code> interfaces provided by the Laravel AI SDK. The <code class="language-plaintext highlighter-rouge">instructions</code> method provides context for the AI model, while the <code class="language-plaintext highlighter-rouge">messages</code> method retrieves the user’s recent orders from the database and formats them as a system message.</p>

<p>When you call the <code class="language-plaintext highlighter-rouge">prompt</code> method with a user query (e.g., <strong>“Where is my last order?”</strong>), the AI model will analyze the provided context and generate a response based on the user’s recent orders.</p>

<h2 id="code-review-bot-for-prs-github--gitlab-integration">Code Review Bot for PRs (GitHub / GitLab Integration)</h2>

<p>You can use the SDK to build an internal <strong>“AI reviewer”</strong> that comments on pull requests in your Laravel monorepo.</p>

<p>At a high level:</p>

<ul>
  <li>
    <p>When a PR is opened or updated, your CI pipeline (or a small Laravel service) fetches the diff and key files.</p>
  </li>
  <li>
    <p>You create an agent like <code class="language-plaintext highlighter-rouge">CodeReviewer</code> whose <code class="language-plaintext highlighter-rouge">instructions()</code> explain your team’s standards: coding style, architecture rules, security expectations, performance guidelines, etc.</p>
  </li>
  <li>
    <p>In <code class="language-plaintext highlighter-rouge">prompt()</code>, you pass a structured prompt: a short summary of the PR, the diff, and maybe a list of changed files as attachments (e.g. <code class="language-plaintext highlighter-rouge">Files\Document::fromString($diff, 'text/plain')</code>).</p>
  </li>
  <li>
    <p>The agent uses structured output to return something like:</p>

    <ul>
      <li>
        <p><code class="language-plaintext highlighter-rouge">summary</code>: overall explanation of changes</p>
      </li>
      <li>
        <p><code class="language-plaintext highlighter-rouge">issues</code>: list of problems with severity and file/line hints</p>
      </li>
      <li>
        <p><code class="language-plaintext highlighter-rouge">suggestions</code>: refactors or tests to add</p>
      </li>
    </ul>
  </li>
  <li>
    <p>Your Laravel app then maps that output to actual comments on the PR via the Git hosting API.</p>
  </li>
</ul>

<p>This leverages agents, attachments, and structured output, but in a domain (code review automation) not explicitly shown in the docs.</p>

<h2 id="personalized-learning--quiz-system-for-an-edtech-app">Personalized Learning / Quiz System for an EdTech App</h2>

<p>Imagine you’re building an online course platform and want smarter, adaptive practice for students.</p>

<p>You could use the AI SDK to:</p>

<ul>
  <li>Generate personalized quizzes from course content</li>
</ul>

<p>When a student finishes a lesson, call an agent like <code class="language-plaintext highlighter-rouge">QuizGenerator</code> with:</p>

<ul>
  <li>
    <p><strong>Instructions:</strong> <em>“You are an exam coach. Generate questions only from the given lesson content, aligned to this student’s weaknesses.”</em></p>
  </li>
  <li>
    <p><strong>Messages:</strong> include a summary of what the student just studied, plus their past quiz results.</p>
  </li>
  <li>
    <p><strong>Attachments:</strong> lesson notes or slides as <code class="language-plaintext highlighter-rouge">Files\Document::fromStorage(...)</code>.The agent uses structured output to return a schema like:￼You then persist these questions in your <code class="language-plaintext highlighter-rouge">quizzes</code> and <code class="language-plaintext highlighter-rouge">quiz_questions</code> tables.</p>
  </li>
  <li>
    <p>Adapt difficulty using conversation context</p>
  </li>
</ul>

<p>Implement <code class="language-plaintext highlighter-rouge">Conversational</code> or <code class="language-plaintext highlighter-rouge">RemembersConversations</code> so the agent can see previous attempts, topics the student struggled with, and feedback given. The next quiz can intentionally focus on weak areas or gradually increase difficulty.</p>

<ul>
  <li>Explain answers on demand</li>
</ul>

<p>A second agent, <code class="language-plaintext highlighter-rouge">AnswerExplainer</code>, takes the student’s wrong answer and the original question as prompt, and returns a tailored explanation (“why B is wrong and C is right”), again via structured output so you can show explanations, hints, and follow‑up questions cleanly in your UI.</p>

<ul>
  <li>
    <p>Enhance with embeddings and reranking</p>
  </li>
  <li>
    <p>Use Embeddings + <code class="language-plaintext highlighter-rouge">whereVectorSimilarTo</code> to find the most relevant paragraphs in your course material for each question.</p>
  </li>
  <li>
    <p>Use Reranking to reorder candidate questions so the most relevant and appropriate ones are presented first.</p>
  </li>
</ul>

<p>This gives you an adaptive learning system built entirely on top of the Laravel AI SDK primitives: agents, structured output, files/attachments, conversation memory, embeddings, and reranking.</p>

<h2 id="in-closing">In closing</h2>

<p>These are just a few examples of how you can use the Laravel AI SDK in real-world applications. The SDK provides a lot of flexibility and power (such as <a href="https://laravel.com/docs/12.x/ai-sdk#remembering-conversations">remembering conversations</a> and <a href="https://laravel.com/docs/12.x/ai-sdk#middleware">middleware</a>), allowing you to build intelligent features that can enhance user experience and provide valuable insights.</p>

<p>Whether you’re mining user data, automating code reviews, or creating personalized learning experiences, the Laravel AI SDK has you covered.</p>]]></content><author><name>Amit Merchant</name></author><category term="Laravel" /><summary type="html"><![CDATA[The Laravel team recently released the Laravel AI SDK that provides a simple and elegant way to integrate AI capabilities into your Laravel applications. The SDK offers a clean and intuitive API that allows developers to easily interact with AI models and services.]]></summary><media:thumbnail xmlns:media="http://search.yahoo.com/mrss/" url="https://www.amitmerchant.com/cdn/real-world-examples-using-laravel-ai-sdk.png" /><media:content medium="image" url="https://www.amitmerchant.com/cdn/real-world-examples-using-laravel-ai-sdk.png" xmlns:media="http://search.yahoo.com/mrss/" /></entry><entry><title type="html">Add UI sounds in your web apps in a heartbeat</title><link href="https://www.amitmerchant.com/add-ui-sounds-in-your-web-apps-in-hearbeat/" rel="alternate" type="text/html" title="Add UI sounds in your web apps in a heartbeat" /><published>2026-01-15T00:00:00+00:00</published><updated>2026-01-15T00:00:00+00:00</updated><id>https://www.amitmerchant.com/add-ui-sounds-in-your-web-apps-in-hearbeat</id><content type="html" xml:base="https://www.amitmerchant.com/add-ui-sounds-in-your-web-apps-in-hearbeat/"><![CDATA[<p>I was reading the article <a href="https://www.userinterface.wiki/sounds-on-the-web">Sounds on The Web</a> by <em>Raphael Salaja</em> the other day. The article explains the importance of sounds in web applications and how integrating them just enough can dramatically enhance user experience by providing feedback and improving usability.</p>

<p>It’s a really well-written piece that I highly recommend you to read. But I was wondering what they are using under the hood to add these sounds since the article itself doesn’t mention any libraries or tools. So, like always, I dug the source code, and I found there’s this magic library called <a href="https://snd.dev">SND</a> (also <a href="https://github.com/snd-lib/snd-lib">open-source</a>) is being used to add these sounds.</p>

<p>Now, it’s pretty interesting because SND is a tiny JavaScript library that makes it super easy to add UI sounds to your web applications. It provides a simple API to play sounds for various UI interactions like button clicks, notifications, warnings, errors, and more.</p>

<p>And as the article title suggests, it literally takes just a <em>“heartbeat”</em> to add sounds to your web apps with SND.</p>

<blockquote>
  <p>SND takes an interesting approach to use the sounds where once the library is included in your app, <strong>you just need to add predefined classes to the HTML elements</strong> where you want the sounds to be played.</p>
</blockquote>

<p>SND lets you choose from three audio kits, all of which have different sound signatures: <strong>sine</strong>, <strong>piano</strong>, and <strong>industrial</strong></p>

<p>So, for instance, you can include the SND library with the <em>sine kit</em> like this:</p>

<div class="language-html highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="nt">&lt;script </span><span class="na">src=</span><span class="s">"https://cdn.jsdelivr.net/gh/snd-lib/snd-lib@v1.2.4/dist/browser/snd.js?kit=01"</span><span class="nt">&gt;&lt;/script&gt;</span>
</code></pre></div></div>

<p>Here, the <code class="language-plaintext highlighter-rouge">kit=01</code> query parameter indicates that we want to use the sine kit. Similarly, you can use <code class="language-plaintext highlighter-rouge">kit=02</code> for the piano kit and <code class="language-plaintext highlighter-rouge">kit=03</code> for the industrial kit.</p>

<p>Next, if you want to play a click sound when a button is clicked, you just need to add the class <code class="language-plaintext highlighter-rouge">snd__button</code> to the button element like this:</p>

<div class="language-html highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="nt">&lt;button</span> <span class="na">class=</span><span class="s">"snd__button"</span><span class="nt">&gt;</span>Click Me<span class="nt">&lt;/button&gt;</span>
</code></pre></div></div>

<p>And the button starts playing a click sound on every click, just like that. No additional JavaScript code is needed!</p>

<p>Here’s a CodePen demo that I created to showcase how SND works.</p>

<p class="codepen" data-height="300" data-slug-hash="xbORXxO" data-pen-title="SND lib demo for UI sounds" data-user="amit_merchant" style="height: 300px; box-sizing: border-box; display: flex; align-items: center; justify-content: center; border: 2px solid; margin: 1em 0; padding: 1em;">
<span>See the Pen <a href="https://codepen.io/amit_merchant/pen/xbORXxO">
SND lib demo for UI sounds</a> by Amit Merchant (<a href="https://codepen.io/amit_merchant">@amit_merchant</a>)
on <a href="https://codepen.io">CodePen</a>.</span>
</p>
<script async="" src="https://public.codepenassets.com/embed/index.js"></script>

<p>The good thing here is that even on repeated clicks, the sound plays without any delay or interruption. All the nitty-gritty of handling audio playback is taken care of by the library itself.</p>

<p>Also, if you want finer-grained control over the sounds, you can use the JavaScript API provided by SND. For example, you can play a sound programmatically like this:</p>

<div class="language-js highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="kd">const</span> <span class="nx">snd</span> <span class="o">=</span> <span class="k">new</span> <span class="nx">Snd</span><span class="p">();</span>

<span class="nx">snd</span><span class="p">.</span><span class="nx">load</span><span class="p">(</span><span class="nx">Snd</span><span class="p">.</span><span class="nx">KITS</span><span class="p">.</span><span class="nx">SND01</span><span class="p">).</span><span class="nx">then</span><span class="p">(()</span> <span class="o">=&gt;</span> <span class="p">{</span>
  <span class="c1">// you can add listeners to UI events:</span>
  <span class="nb">document</span><span class="p">.</span><span class="nx">getElementById</span><span class="p">(</span><span class="dl">"</span><span class="s2">btn</span><span class="dl">"</span><span class="p">).</span><span class="nx">addEventListener</span><span class="p">(</span><span class="dl">"</span><span class="s2">click</span><span class="dl">"</span><span class="p">,</span> <span class="p">()</span> <span class="o">=&gt;</span> <span class="p">{</span>
    <span class="c1">// snd.play(Snd.SOUNDS.CELEBRATION);</span>
    <span class="nx">snd</span><span class="p">.</span><span class="nx">playNotification</span><span class="p">();</span>
  <span class="p">});</span>
<span class="p">});</span>
</code></pre></div></div>

<p>One more thing I noticed about SND is that it uses something called <strong>audio sprites</strong> (like <a href="https://en.wikipedia.org/wiki/Sprite_(computer_graphics)">image stripes</a>) to optimize the loading and playback of sounds. Instead of loading individual sound files for each UI interaction, SND combines multiple sounds into <a href="https://cdn.jsdelivr.net/gh/snd-lib/snd-lib@v1.2.4/assets/sounds/sprite/01/audioSprite.mp3">a single audio file</a> (sprite) and plays specific segments of that file as needed. This approach reduces the number of HTTP requests and improves performance.</p>

<p>Overall, I’m quite impressed with SND and would definitely consider using it in my future web projects that require UI sounds.</p>]]></content><author><name>Amit Merchant</name></author><category term="UI" /><summary type="html"><![CDATA[I was reading the article Sounds on The Web by Raphael Salaja the other day. The article explains the importance of sounds in web applications and how integrating them just enough can dramatically enhance user experience by providing feedback and improving usability.]]></summary><media:thumbnail xmlns:media="http://search.yahoo.com/mrss/" url="https://www.amitmerchant.com/cdn/add-ui-sounds-in-your-web-apps-in-hearbeat.png" /><media:content medium="image" url="https://www.amitmerchant.com/cdn/add-ui-sounds-in-your-web-apps-in-hearbeat.png" xmlns:media="http://search.yahoo.com/mrss/" /></entry><entry><title type="html">SVG Filters are just amazing!</title><link href="https://www.amitmerchant.com/svg-filters-are-just-amazing/" rel="alternate" type="text/html" title="SVG Filters are just amazing!" /><published>2026-01-12T00:00:00+00:00</published><updated>2026-01-12T00:00:00+00:00</updated><id>https://www.amitmerchant.com/svg-filters-are-just-amazing</id><content type="html" xml:base="https://www.amitmerchant.com/svg-filters-are-just-amazing/"><![CDATA[<p>So, lately, I have been exploring <a href="https://haleypark.design/">Haley Park’s website</a> and it’s pretty cute and well designed. What caught my attention, though, was this water ripple effect on one of the text snippets.</p>

<p>Here’s what it looks like.</p>

<div class="text-container">
    <p class="ripple-text">Breathe</p>
</div>
<svg style="position: absolute; width: 0px; height: 0px; pointer-events: none;">
    <filter id="water-ripple">
        <feTurbulence type="fractalNoise" baseFrequency="0.05" numOctaves="2" result="ripple">
        <animate attributeName="baseFrequency" dur="10s" values="0.02;0.05;0.02" repeatCount="indefinite"></animate>
        </feTurbulence>
        <feDisplacementMap in="SourceGraphic" in2="ripple" scale="5"></feDisplacementMap>
    </filter>
</svg>
<style>
.text-container {
    display: flex;
    justify-content: center;
    align-items: center;
}
.ripple-text {
    position: relative;
    filter: url(#water-ripple);
    display: inline-block;
    font-size: 3rem;
    font-weight: bold;
    color: #0e6cb6;
    font-family: "Iowan Old Style", "Palatino Linotype", "URW Palladio L", P052, serif;
}
</style>

<p>As you can see, the text features a beautiful water ripple effect. I dug through the source code and found out that it’s done using the <a href="https://developer.mozilla.org/en-US/docs/Web/SVG/Guides/SVG_filters">SVG filters</a>!</p>

<p>So, I learned a bit about SVG filters, and here’s what I found out.</p>

<p>SVG filters are a powerful way to apply graphical effects to SVG elements. They can also be applied to HTML elements using the <code class="language-plaintext highlighter-rouge">filter</code> CSS property. Without SVG filters, achieving complex visual effects like the water ripple effect would require using images or complex JavaScript animations.</p>

<p>To recreate the water ripple effect, first, define the element you want to apply the effect to. In this case, it’s a simple paragraph with the text <strong>“Breathe”</strong>.</p>

<div class="language-html highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="nt">&lt;p</span> <span class="na">class=</span><span class="s">"ripple-text"</span><span class="nt">&gt;</span>
    Breathe
<span class="nt">&lt;/p&gt;</span>
</code></pre></div></div>

<p>Next, define the SVG filter that you want to apply.</p>

<div class="language-html highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="nt">&lt;svg</span> <span class="na">style=</span><span class="s">"position: absolute; width: 0px; height: 0px; pointer-events: none;"</span><span class="nt">&gt;</span>
    <span class="nt">&lt;filter</span> <span class="na">id=</span><span class="s">"water-ripple"</span><span class="nt">&gt;</span>
        <span class="nt">&lt;feTurbulence</span> <span class="na">type=</span><span class="s">"fractalNoise"</span> <span class="na">baseFrequency=</span><span class="s">"0.05"</span> <span class="na">numOctaves=</span><span class="s">"2"</span> <span class="na">result=</span><span class="s">"ripple"</span><span class="nt">&gt;</span>
            <span class="nt">&lt;animate</span> <span class="na">attributeName=</span><span class="s">"baseFrequency"</span> <span class="na">dur=</span><span class="s">"10s"</span> <span class="na">values=</span><span class="s">"0.02;0.05;0.02"</span> <span class="na">repeatCount=</span><span class="s">"indefinite"</span><span class="nt">&gt;&lt;/animate&gt;</span>
        <span class="nt">&lt;/feTurbulence&gt;</span>
        <span class="nt">&lt;feDisplacementMap</span> <span class="na">in=</span><span class="s">"SourceGraphic"</span> <span class="na">in2=</span><span class="s">"ripple"</span> <span class="na">scale=</span><span class="s">"5"</span><span class="nt">&gt;&lt;/feDisplacementMap&gt;</span>
    <span class="nt">&lt;/filter&gt;</span>
<span class="nt">&lt;/svg&gt;</span>
</code></pre></div></div>

<p>As you can tell, we can define a filter using the <code class="language-plaintext highlighter-rouge">&lt;filter&gt;</code> element with id <code class="language-plaintext highlighter-rouge">water-ripple</code>.</p>

<p>Inside the filter, we use two filter primitives:</p>

<ul>
  <li><strong>The <code class="language-plaintext highlighter-rouge">&lt;feTurbulence&gt;</code> element</strong> - It’s used to create a noise texture, which is then animated to simulate the water ripple effect.</li>
  <li><strong>The <code class="language-plaintext highlighter-rouge">&lt;feDisplacementMap&gt;</code> element</strong> - It’s used to apply the turbulence effect to the text.</li>
</ul>

<p>There are a bunch of parameters on these filters that you can tweak to achieve different effects.</p>

<p>For instance, in the <code class="language-plaintext highlighter-rouge">&lt;feTurbulence&gt;</code> element, we set the <code class="language-plaintext highlighter-rouge">type</code> to <code class="language-plaintext highlighter-rouge">fractalNoise</code>, which creates a fractal noise pattern. The <code class="language-plaintext highlighter-rouge">baseFrequency</code> controls the frequency of the noise, and <code class="language-plaintext highlighter-rouge">numOctaves</code> controls the complexity of the noise.</p>

<p>Similarly, for the <code class="language-plaintext highlighter-rouge">&lt;feDisplacementMap&gt;</code> element, the <code class="language-plaintext highlighter-rouge">in</code> attribute specifies the input image (in this case, the original text), and the <code class="language-plaintext highlighter-rouge">in2</code> attribute specifies the turbulence effect created by <code class="language-plaintext highlighter-rouge">&lt;feTurbulence&gt;</code>. The <code class="language-plaintext highlighter-rouge">scale</code> attribute controls the amount of displacement applied to the text.</p>

<p>Also, the SVG is hidden from viewing by setting its width and height to 0 and positioning it absolutely.</p>

<blockquote>
  <p><strong>Fun fact:</strong> The <code class="language-plaintext highlighter-rouge">fe</code> in the filter primitive names stands for <em>“filter effect”</em>.</p>
</blockquote>

<p>Finally, apply the filter to the text by using the <code class="language-plaintext highlighter-rouge">id</code> of the filter in the CSS.</p>

<div class="language-css highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="nc">.ripple-text</span> <span class="p">{</span>
    <span class="nl">filter</span><span class="p">:</span> <span class="sx">url(#water-ripple)</span><span class="p">;</span>
    <span class="nl">font-size</span><span class="p">:</span> <span class="m">3rem</span><span class="p">;</span>
    <span class="nl">font-weight</span><span class="p">:</span> <span class="nb">bold</span><span class="p">;</span>
<span class="p">}</span>
</code></pre></div></div>

<p>And that’s it! You now have a beautiful water ripple effect applied to your text using SVG filters.</p>

<p>I found this cool <a href="https://yoksel.github.io/svg-filters/">SVG filter playground</a> where you can experiment with different filter primitives and see the effects in real-time. You can stack multiple filter primitives, change their parameters, and create complex effects.</p>

<p>And if you want to learn more about SVG filters, check out the <a href="https://developer.mozilla.org/en-US/docs/Web/SVG/Guides/SVG_filters">MDN documentation on SVG filters</a> for a comprehensive guide on how to use them.</p>]]></content><author><name>Amit Merchant</name></author><category term="CSS" /><summary type="html"><![CDATA[So, lately, I have been exploring Haley Park’s website and it’s pretty cute and well designed. What caught my attention, though, was this water ripple effect on one of the text snippets.]]></summary><media:thumbnail xmlns:media="http://search.yahoo.com/mrss/" url="https://www.amitmerchant.com/cdn/svg-filters-are-just-amazing.png" /><media:content medium="image" url="https://www.amitmerchant.com/cdn/svg-filters-are-just-amazing.png" xmlns:media="http://search.yahoo.com/mrss/" /></entry><entry><title type="html">The new clamp() function in PHP 8.6</title><link href="https://www.amitmerchant.com/the-clamp-function-in-php-86/" rel="alternate" type="text/html" title="The new clamp() function in PHP 8.6" /><published>2025-12-12T00:00:00+00:00</published><updated>2025-12-12T00:00:00+00:00</updated><id>https://www.amitmerchant.com/the-clamp-function-in-php-86</id><content type="html" xml:base="https://www.amitmerchant.com/the-clamp-function-in-php-86/"><![CDATA[<p>You know how sometimes you want to ensure that a value stays within a specific range? Maybe you’re working with user input, configuration values, or any scenario where you need to enforce boundaries.</p>

<p>In scenarios like these, having a built-in function to clamp values can be incredibly useful. Well, good news! PHP 8.6 <a href="https://wiki.php.net/rfc/clamp_v2">will be introducing</a> a new function called <code class="language-plaintext highlighter-rouge">clamp()</code> that does exactly that.</p>

<ul id="markdown-toc">
  <li><a href="#what-is-the-clamp-function" id="markdown-toc-what-is-the-clamp-function">What is the <code class="language-plaintext highlighter-rouge">clamp()</code> function?</a></li>
  <li><a href="#clamp-with-named-parameters" id="markdown-toc-clamp-with-named-parameters">Clamp with named parameters</a></li>
  <li><a href="#real-world-use-cases" id="markdown-toc-real-world-use-cases">Real-world use cases</a></li>
  <li><a href="#conclusion" id="markdown-toc-conclusion">Conclusion</a></li>
</ul>

<h2 id="what-is-the-clamp-function">What is the <code class="language-plaintext highlighter-rouge">clamp()</code> function?</h2>

<p>PHP 8.6’s <code class="language-plaintext highlighter-rouge">clamp()</code> function allows you to restrict a value to be within a specified minimum and maximum range. If the value is less than the minimum, it returns the minimum; if it’s greater than the maximum, it returns the maximum; otherwise, it returns the value itself.</p>

<p>Here’s the signature of the function:</p>

<div class="language-php highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="nf">clamp</span> <span class="p">(</span> <span class="n">mixed</span> <span class="nv">$value</span><span class="p">,</span> <span class="n">mixed</span> <span class="nv">$min</span><span class="p">,</span> <span class="n">mixed</span> <span class="nv">$max</span> <span class="p">)</span> <span class="o">:</span> <span class="n">mixed</span>
</code></pre></div></div>

<p>As you can tell, clamp takes three arguments, a <code class="language-plaintext highlighter-rouge">$value</code>, <code class="language-plaintext highlighter-rouge">$min</code>, and <code class="language-plaintext highlighter-rouge">$max</code>, then checks if $value is within the bounds of <code class="language-plaintext highlighter-rouge">$min</code> and <code class="language-plaintext highlighter-rouge">$max</code> (both inclusive).</p>

<ul>
  <li>If <code class="language-plaintext highlighter-rouge">$value</code> is less than <code class="language-plaintext highlighter-rouge">$min</code>, it returns <code class="language-plaintext highlighter-rouge">$min</code>.</li>
  <li>If <code class="language-plaintext highlighter-rouge">$value</code> is greater than <code class="language-plaintext highlighter-rouge">$max</code>, it returns <code class="language-plaintext highlighter-rouge">$max</code>.</li>
  <li>If <code class="language-plaintext highlighter-rouge">$value</code> is between <code class="language-plaintext highlighter-rouge">$min</code> and <code class="language-plaintext highlighter-rouge">$max</code>, it returns <code class="language-plaintext highlighter-rouge">$value</code>.</li>
</ul>

<p>It throws a <code class="language-plaintext highlighter-rouge">ValueError</code> if <strong>min</strong> &gt; <strong>max</strong> or if min/max are <strong>NAN</strong>.</p>

<p>Here’s a simple example of how it works:</p>

<div class="language-php highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="nv">$value1</span> <span class="o">=</span> <span class="nf">clamp</span><span class="p">(</span><span class="mi">15</span><span class="p">,</span> <span class="mi">10</span><span class="p">,</span> <span class="mi">20</span><span class="p">);</span> <span class="c1">// Returns 15</span>
<span class="nv">$value2</span> <span class="o">=</span> <span class="nf">clamp</span><span class="p">(</span><span class="mi">5</span><span class="p">,</span> <span class="mi">10</span><span class="p">,</span> <span class="mi">20</span><span class="p">);</span>  <span class="c1">// Returns 10</span>
<span class="nv">$value3</span> <span class="o">=</span> <span class="nf">clamp</span><span class="p">(</span><span class="mi">25</span><span class="p">,</span> <span class="mi">10</span><span class="p">,</span> <span class="mi">20</span><span class="p">);</span> <span class="c1">// Returns 20</span>
</code></pre></div></div>

<blockquote>
  <p><strong>Fun fact:</strong> A long time ago, I wrote a <a href="/a-simple-clamp-function-in-php/">custom clamp function</a> in PHP as a utility function for my projects.</p>
</blockquote>

<h2 id="clamp-with-named-parameters">Clamp with named parameters</h2>

<p>The clamp function becomes even more straight-forward with <a href="/proposed-named-arguments-php/">named parameters</a> and re-ordering of the arguments.</p>

<div class="language-php highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="nv">$brightness</span> <span class="o">=</span> <span class="nf">clamp</span><span class="p">(</span><span class="n">min</span><span class="o">:</span> <span class="mi">0</span><span class="p">,</span> <span class="n">value</span><span class="o">:</span> <span class="nv">$brightness</span><span class="p">,</span> <span class="n">max</span><span class="o">:</span> <span class="mi">100</span><span class="p">);</span>
</code></pre></div></div>

<h2 id="real-world-use-cases">Real-world use cases</h2>

<p>Here are some practical scenarios where the <code class="language-plaintext highlighter-rouge">clamp()</code> function can come in handy.</p>

<p><strong>User input:</strong> keep a percentage between 0 and 100</p>

<div class="language-php highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="nv">$percentage</span> <span class="o">=</span> <span class="nf">clamp</span><span class="p">(</span><span class="nv">$percentage</span><span class="p">,</span> <span class="mi">0</span><span class="p">,</span> <span class="mi">100</span><span class="p">);</span>
</code></pre></div></div>

<p><strong>UI sliders:</strong> constrain a volume setting between 0 and 10</p>

<div class="language-php highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="nv">$volume</span> <span class="o">=</span> <span class="nf">clamp</span><span class="p">(</span><span class="nv">$volume</span><span class="p">,</span> <span class="mi">0</span><span class="p">,</span> <span class="mi">10</span><span class="p">);</span>
</code></pre></div></div>

<p><strong>Pagination:</strong> bound page number between the first and last page</p>

<div class="language-php highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="nv">$page</span> <span class="o">=</span> <span class="nf">clamp</span><span class="p">((</span><span class="n">int</span><span class="p">)</span><span class="nv">$_GET</span><span class="p">[</span><span class="s1">'page'</span><span class="p">]</span> <span class="o">??</span> <span class="mi">1</span><span class="p">,</span> <span class="mi">1</span><span class="p">,</span> <span class="nv">$totalPages</span><span class="p">);</span>
</code></pre></div></div>

<p><strong>Rate limiting:</strong> prevent burst count from exceeding a ceiling</p>

<div class="language-php highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="nv">$requests</span> <span class="o">=</span> <span class="nf">clamp</span><span class="p">(</span><span class="nv">$requests</span><span class="p">,</span> <span class="mi">0</span><span class="p">,</span> <span class="nv">$maxBurst</span><span class="p">);</span>
</code></pre></div></div>

<p><strong>Dates:</strong> ensure a booking date falls within an allowed window</p>

<div class="language-php highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="nv">$date</span> <span class="o">=</span> <span class="k">new</span> <span class="nc">DateTimeImmutable</span><span class="p">(</span><span class="nv">$input</span><span class="p">);</span>
<span class="nv">$start</span> <span class="o">=</span> <span class="k">new</span> <span class="nc">DateTimeImmutable</span><span class="p">(</span><span class="s1">'2025-08-15'</span><span class="p">);</span>
<span class="nv">$end</span>   <span class="o">=</span> <span class="k">new</span> <span class="nc">DateTimeImmutable</span><span class="p">(</span><span class="s1">'2025-09-15'</span><span class="p">);</span>
<span class="nv">$clamped</span> <span class="o">=</span> <span class="nf">clamp</span><span class="p">(</span><span class="nv">$date</span><span class="p">,</span> <span class="nv">$start</span><span class="p">,</span> <span class="nv">$end</span><span class="p">);</span> <span class="c1">// returns start/end/date accordingly</span>
</code></pre></div></div>

<p><strong>Geometry:</strong> restrict an angle between 0 and 90</p>

<div class="language-php highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="nv">$angle</span> <span class="o">=</span> <span class="nf">clamp</span><span class="p">(</span><span class="nv">$angle</span><span class="p">,</span> <span class="mi">0</span><span class="p">,</span> <span class="mi">90</span><span class="p">);</span>
</code></pre></div></div>

<p><strong>Strings (lexicographic):</strong> keep a tag between <em>“c”</em> and <em>“g”</em></p>

<div class="language-php highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="nv">$tag</span> <span class="o">=</span> <span class="nf">clamp</span><span class="p">(</span><span class="nv">$tag</span><span class="p">,</span> <span class="s2">"c"</span><span class="p">,</span> <span class="s2">"g"</span><span class="p">);</span>
</code></pre></div></div>

<h2 id="conclusion">Conclusion</h2>

<p>The <code class="language-plaintext highlighter-rouge">clamp()</code> function in PHP 8.6 is a simple yet powerful addition that can help you manage values effectively by enforcing boundaries. Whether you’re dealing with user input, configuration settings, or any scenario where you need to ensure values stay within a specific range, <code class="language-plaintext highlighter-rouge">clamp()</code> provides a clean and efficient solution.</p>

<p>Read more about the <code class="language-plaintext highlighter-rouge">clamp()</code> function in <a href="https://wiki.php.net/rfc/clamp_v2">this RFC</a>.</p>]]></content><author><name>Amit Merchant</name></author><category term="PHP" /><summary type="html"><![CDATA[You know how sometimes you want to ensure that a value stays within a specific range? Maybe you’re working with user input, configuration values, or any scenario where you need to enforce boundaries.]]></summary><media:thumbnail xmlns:media="http://search.yahoo.com/mrss/" url="https://www.amitmerchant.com/cdn/the-clamp-function-php-86.png" /><media:content medium="image" url="https://www.amitmerchant.com/cdn/the-clamp-function-php-86.png" xmlns:media="http://search.yahoo.com/mrss/" /></entry></feed>