<?xml version="1.0" encoding="UTF-8"?>
<rss version="2.0" xmlns:atom="http://www.w3.org/2005/Atom" xmlns:dc="http://purl.org/dc/elements/1.1/">
  <channel>
    <title>DEV Community: QRflows</title>
    <description>The latest articles on DEV Community by QRflows (@qrflows).</description>
    <link>https://dev.to/qrflows</link>
    <image>
      <url>https://media2.dev.to/dynamic/image/width=90,height=90,fit=cover,gravity=auto,format=auto/https:%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Fuser%2Fprofile_image%2F3947933%2F278784b4-82c7-4b16-bacd-1e2a681ac75d.png</url>
      <title>DEV Community: QRflows</title>
      <link>https://dev.to/qrflows</link>
    </image>
    <atom:link rel="self" type="application/rss+xml" href="https://dev.to/feed/qrflows"/>
    <language>en</language>
    <item>
      <title>Google Translate was crashing our React app — here's how we fixed "removeChild" for good</title>
      <dc:creator>QRflows</dc:creator>
      <pubDate>Thu, 29 Oct 2026 15:14:40 +0000</pubDate>
      <link>https://dev.to/qrflows/google-translate-was-crashing-our-react-app-heres-how-we-fixed-removechild-for-good-32hi</link>
      <guid>https://dev.to/qrflows/google-translate-was-crashing-our-react-app-heres-how-we-fixed-removechild-for-good-32hi</guid>
      <description>&lt;p&gt;I build &lt;a href="https://qrflows.app" rel="noopener noreferrer"&gt;QRflows&lt;/a&gt;, a dynamic QR code platform. Last week I was going through session recordings and found our most engaged user of the month: someone in Hong Kong who spent 11 minutes in the dashboard, clicked 82 times, tried half a dozen QR types, added a logo, built a landing page.&lt;/p&gt;

&lt;p&gt;At minute three, the page crashed and reloaded.&lt;/p&gt;

&lt;p&gt;The error in the console:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;NotFoundError: Failed to execute 'removeChild' on 'Node':
The node to be removed is not a child of this node.
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The recording made the cause obvious once I noticed it: the whole dashboard was in Chinese. Our site only exists in English. The user had Chrome's built-in Google Translate turned on.&lt;/p&gt;

&lt;p&gt;This is one of those bugs that never shows up in your own testing, because you never translate your own app. Here's what's going on, how to reproduce it without Google Translate, and the three-layer fix we shipped.&lt;/p&gt;




&lt;h2&gt;
  
  
  Why Google Translate breaks React
&lt;/h2&gt;

&lt;p&gt;React keeps references to the DOM nodes it created, including plain text nodes. When state changes, it updates or removes those exact nodes.&lt;/p&gt;

&lt;p&gt;Google Translate doesn't know about React. When it translates a page, it walks the DOM and &lt;strong&gt;replaces text nodes&lt;/strong&gt; with its own &lt;code&gt;&amp;lt;font&amp;gt;&lt;/code&gt; elements containing the translated text:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight html"&gt;&lt;code&gt;&lt;span class="c"&gt;&amp;lt;!-- What React rendered --&amp;gt;&lt;/span&gt;
&lt;span class="nt"&gt;&amp;lt;button&amp;gt;&lt;/span&gt;Save&lt;span class="nt"&gt;&amp;lt;/button&amp;gt;&lt;/span&gt;

&lt;span class="c"&gt;&amp;lt;!-- What the DOM looks like after Google Translate --&amp;gt;&lt;/span&gt;
&lt;span class="nt"&gt;&amp;lt;button&amp;gt;&amp;lt;font&amp;gt;&amp;lt;font&amp;gt;&lt;/span&gt;保存&lt;span class="nt"&gt;&amp;lt;/font&amp;gt;&amp;lt;/font&amp;gt;&amp;lt;/button&amp;gt;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The original &lt;code&gt;"Save"&lt;/code&gt; text node is gone from the DOM, but React still holds a reference to it. The next time React needs to remove or replace that text, it calls &lt;code&gt;parent.removeChild(textNode)&lt;/code&gt; — and the browser throws, because that node is no longer a child of anything.&lt;/p&gt;

&lt;p&gt;The same thing happens with &lt;code&gt;insertBefore&lt;/code&gt; when React tries to insert something next to a node that Translate has swapped out.&lt;/p&gt;

&lt;p&gt;It's a long-standing issue — &lt;a href="https://github.com/facebook/react/issues/11538" rel="noopener noreferrer"&gt;facebook/react#11538&lt;/a&gt; has been open since 2017 — and it isn't specific to React: anything that holds references to text nodes is vulnerable. If you want the deep dive, &lt;a href="https://martijnhols.nl/gists/everything-about-google-translate-crashing-react" rel="noopener noreferrer"&gt;Martijn Hols' write-up&lt;/a&gt; is excellent.&lt;/p&gt;

&lt;h3&gt;
  
  
  Which code is vulnerable
&lt;/h3&gt;

&lt;p&gt;The crash needs two things: a &lt;strong&gt;bare text node&lt;/strong&gt; and something &lt;strong&gt;conditional&lt;/strong&gt; next to it. Patterns like these:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight jsx"&gt;&lt;code&gt;&lt;span class="c1"&gt;// Text sitting next to a conditional element&lt;/span&gt;
&lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nt"&gt;button&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;&lt;/span&gt;
  &lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="nx"&gt;isSaving&lt;/span&gt; &lt;span class="o"&gt;&amp;amp;&amp;amp;&lt;/span&gt; &lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nc"&gt;Spinner&lt;/span&gt; &lt;span class="p"&gt;/&amp;gt;&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;
  Save
&lt;span class="p"&gt;&amp;lt;/&lt;/span&gt;&lt;span class="nt"&gt;button&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;&lt;/span&gt;

&lt;span class="c1"&gt;// Conditional text&lt;/span&gt;
&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="nx"&gt;error&lt;/span&gt; &lt;span class="o"&gt;&amp;amp;&amp;amp;&lt;/span&gt; &lt;span class="nx"&gt;error&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;message&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;

&lt;span class="c1"&gt;// Ternary returning strings&lt;/span&gt;
&lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nt"&gt;span&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;&lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="nx"&gt;isSaving&lt;/span&gt; &lt;span class="p"&gt;?&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;Saving…&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt; &lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;Saved&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="p"&gt;&amp;lt;/&lt;/span&gt;&lt;span class="nt"&gt;span&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;&lt;/span&gt;

&lt;span class="c1"&gt;// Text mixed with an expression&lt;/span&gt;
&lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nt"&gt;p&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;&lt;/span&gt;Uploaded &lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="nx"&gt;count&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt; files&lt;span class="p"&gt;&amp;lt;/&lt;/span&gt;&lt;span class="nt"&gt;p&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Every QR builder has dozens of these: status labels, counters, "Uploading… / Done", plan badges. Which is why the crash hit our most active user and nobody else.&lt;/p&gt;




&lt;h2&gt;
  
  
  Reproducing it without Google Translate
&lt;/h2&gt;

&lt;p&gt;Waiting for a translated session to show up in your logs is not a test plan. You can simulate what Translate does to the DOM in the browser console:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight javascript"&gt;&lt;code&gt;&lt;span class="kd"&gt;function&lt;/span&gt; &lt;span class="nf"&gt;fakeTranslate&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;root&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nb"&gt;document&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;body&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;walker&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nb"&gt;document&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;createTreeWalker&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;root&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;NodeFilter&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;SHOW_TEXT&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="na"&gt;acceptNode&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;node&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt;
      &lt;span class="nx"&gt;node&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;nodeValue&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;trim&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="o"&gt;&amp;amp;&amp;amp;&lt;/span&gt;
      &lt;span class="nx"&gt;node&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;parentElement&lt;/span&gt; &lt;span class="o"&gt;&amp;amp;&amp;amp;&lt;/span&gt;
      &lt;span class="o"&gt;!&lt;/span&gt;&lt;span class="nx"&gt;node&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;parentElement&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;closest&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;script, style, [translate="no"], font&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
        &lt;span class="p"&gt;?&lt;/span&gt; &lt;span class="nx"&gt;NodeFilter&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;FILTER_ACCEPT&lt;/span&gt;
        &lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;NodeFilter&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;FILTER_REJECT&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="p"&gt;});&lt;/span&gt;

  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;nodes&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;[];&lt;/span&gt;
  &lt;span class="k"&gt;while &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;walker&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;nextNode&lt;/span&gt;&lt;span class="p"&gt;())&lt;/span&gt; &lt;span class="nx"&gt;nodes&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;push&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;walker&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;currentNode&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;

  &lt;span class="k"&gt;for &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;textNode&lt;/span&gt; &lt;span class="k"&gt;of&lt;/span&gt; &lt;span class="nx"&gt;nodes&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;outer&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nb"&gt;document&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;createElement&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;font&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
    &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;inner&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nb"&gt;document&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;createElement&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;font&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
    &lt;span class="nx"&gt;inner&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;textContent&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;译&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt; &lt;span class="o"&gt;+&lt;/span&gt; &lt;span class="nx"&gt;textNode&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;nodeValue&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="c1"&gt;// mark it as "translated"&lt;/span&gt;
    &lt;span class="nx"&gt;outer&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;appendChild&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;inner&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
    &lt;span class="nx"&gt;textNode&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;parentNode&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;replaceChild&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;outer&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;textNode&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
  &lt;span class="p"&gt;}&lt;/span&gt;
  &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="nx"&gt;nodes&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;length&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;

&lt;span class="nb"&gt;window&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;addEventListener&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;error&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;e&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="nx"&gt;console&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;log&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;CRASH:&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;e&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;message&lt;/span&gt;&lt;span class="p"&gt;));&lt;/span&gt;
&lt;span class="nf"&gt;fakeTranslate&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Paste it, run &lt;code&gt;fakeTranslate()&lt;/code&gt;, then use the app: switch tabs, toggle options, type into inputs, navigate between routes. Run &lt;code&gt;fakeTranslate()&lt;/code&gt; again after each step, because real Translate keeps re-translating new content as it appears.&lt;/p&gt;

&lt;p&gt;On our dashboard, switching QR types after this reproduced the exact crash from the recording within a few clicks.&lt;/p&gt;




&lt;h2&gt;
  
  
  The fix: three layers
&lt;/h2&gt;

&lt;p&gt;There's no single clean fix. What worked for us is three layers, from "never crash" to "fewer weird states".&lt;/p&gt;

&lt;h3&gt;
  
  
  Layer 1: a DOM guard so the crash can't happen
&lt;/h3&gt;

&lt;p&gt;This is the workaround from the React issue thread: patch &lt;code&gt;removeChild&lt;/code&gt; and &lt;code&gt;insertBefore&lt;/code&gt; so that, instead of throwing, they skip the operation when the node relationship is no longer what React expects.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;&lt;span class="c1"&gt;// src/lib/domTranslateGuard.ts&lt;/span&gt;
&lt;span class="k"&gt;export&lt;/span&gt; &lt;span class="kd"&gt;function&lt;/span&gt; &lt;span class="nf"&gt;installDomTranslateGuard&lt;/span&gt;&lt;span class="p"&gt;():&lt;/span&gt; &lt;span class="k"&gt;void&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="k"&gt;if &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="k"&gt;typeof&lt;/span&gt; &lt;span class="nb"&gt;window&lt;/span&gt; &lt;span class="o"&gt;===&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;undefined&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt; &lt;span class="o"&gt;||&lt;/span&gt; &lt;span class="k"&gt;typeof&lt;/span&gt; &lt;span class="nx"&gt;Node&lt;/span&gt; &lt;span class="o"&gt;!==&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;function&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;return&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;proto&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;Node&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;prototype&lt;/span&gt; &lt;span class="kd"&gt;as &lt;/span&gt;&lt;span class="kr"&gt;any&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
  &lt;span class="k"&gt;if &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;proto&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;__translateGuardInstalled&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;return&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
  &lt;span class="nx"&gt;proto&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;__translateGuardInstalled&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="kc"&gt;true&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;originalRemoveChild&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;proto&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;removeChild&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
  &lt;span class="nx"&gt;proto&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;removeChild&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;function &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="na"&gt;child&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;Node&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="k"&gt;if &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;child&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;parentNode&lt;/span&gt; &lt;span class="o"&gt;!==&lt;/span&gt; &lt;span class="k"&gt;this&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
      &lt;span class="nx"&gt;console&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;warn&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;[dom-guard] removeChild skipped&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;child&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="k"&gt;this&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
      &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nb"&gt;window&lt;/span&gt; &lt;span class="kd"&gt;as &lt;/span&gt;&lt;span class="kr"&gt;any&lt;/span&gt;&lt;span class="p"&gt;).&lt;/span&gt;&lt;span class="nx"&gt;clarity&lt;/span&gt;&lt;span class="p"&gt;?.(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;event&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;dom_guard_removeChild&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
      &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="nx"&gt;child&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt;
    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="nx"&gt;originalRemoveChild&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;call&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="k"&gt;this&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;child&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
  &lt;span class="p"&gt;};&lt;/span&gt;

  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;originalInsertBefore&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;proto&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;insertBefore&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
  &lt;span class="nx"&gt;proto&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;insertBefore&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;function &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="na"&gt;newNode&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;Node&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="na"&gt;referenceNode&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;Node&lt;/span&gt; &lt;span class="o"&gt;|&lt;/span&gt; &lt;span class="kc"&gt;null&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="k"&gt;if &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;referenceNode&lt;/span&gt; &lt;span class="o"&gt;&amp;amp;&amp;amp;&lt;/span&gt; &lt;span class="nx"&gt;referenceNode&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;parentNode&lt;/span&gt; &lt;span class="o"&gt;!==&lt;/span&gt; &lt;span class="k"&gt;this&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
      &lt;span class="nx"&gt;console&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;warn&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;[dom-guard] insertBefore skipped&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;referenceNode&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="k"&gt;this&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
      &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nb"&gt;window&lt;/span&gt; &lt;span class="kd"&gt;as &lt;/span&gt;&lt;span class="kr"&gt;any&lt;/span&gt;&lt;span class="p"&gt;).&lt;/span&gt;&lt;span class="nx"&gt;clarity&lt;/span&gt;&lt;span class="p"&gt;?.(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;event&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;dom_guard_insertBefore&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
      &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="nx"&gt;newNode&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt;
    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="nx"&gt;originalInsertBefore&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;call&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="k"&gt;this&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;newNode&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;referenceNode&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
  &lt;span class="p"&gt;};&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Two details that matter:&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Install it before React touches the DOM.&lt;/strong&gt; We call it as the very first thing in the client entry, before &lt;code&gt;hydrateRoot&lt;/code&gt;:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;&lt;span class="c1"&gt;// entry-client.tsx&lt;/span&gt;
&lt;span class="k"&gt;import&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;installDomTranslateGuard&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="k"&gt;from&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;./lib/domTranslateGuard&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="nf"&gt;installDomTranslateGuard&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;

&lt;span class="c1"&gt;// ...then hydrateRoot(...)&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;Keep it out of SSR.&lt;/strong&gt; It only lives in the client entry, and the &lt;code&gt;typeof window&lt;/code&gt; check is a second line of defence. The server never touches &lt;code&gt;Node.prototype&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;The trade-off is honest: this hides the error instead of fixing the cause. A skipped &lt;code&gt;insertBefore&lt;/code&gt; can occasionally leave a piece of UI missing until the next render. That's why we log it and send a custom event to our session analytics — so we can see how often the guard actually fires, and on which pages.&lt;/p&gt;

&lt;h3&gt;
  
  
  Layer 2: stop giving Translate bare text nodes
&lt;/h3&gt;

&lt;p&gt;The guard prevents crashes. Wrapping text in elements prevents most of the situations where it would have to step in. If text lives inside its own &lt;code&gt;&amp;lt;span&amp;gt;&lt;/code&gt;, Translate replaces the text &lt;em&gt;inside&lt;/em&gt; the span, and React only ever adds or removes the span itself — which is still there.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight jsx"&gt;&lt;code&gt;&lt;span class="c1"&gt;// Before&lt;/span&gt;
&lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nt"&gt;button&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;&lt;/span&gt;
  &lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="nx"&gt;isSaving&lt;/span&gt; &lt;span class="o"&gt;&amp;amp;&amp;amp;&lt;/span&gt; &lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nc"&gt;Spinner&lt;/span&gt; &lt;span class="p"&gt;/&amp;gt;&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;
  Save
&lt;span class="p"&gt;&amp;lt;/&lt;/span&gt;&lt;span class="nt"&gt;button&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;&lt;/span&gt;

&lt;span class="c1"&gt;// After&lt;/span&gt;
&lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nt"&gt;button&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;&lt;/span&gt;
  &lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="nx"&gt;isSaving&lt;/span&gt; &lt;span class="o"&gt;&amp;amp;&amp;amp;&lt;/span&gt; &lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nc"&gt;Spinner&lt;/span&gt; &lt;span class="p"&gt;/&amp;gt;&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;
  &lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nt"&gt;span&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;&lt;/span&gt;Save&lt;span class="p"&gt;&amp;lt;/&lt;/span&gt;&lt;span class="nt"&gt;span&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;&lt;/span&gt;
&lt;span class="p"&gt;&amp;lt;/&lt;/span&gt;&lt;span class="nt"&gt;button&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;





&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight jsx"&gt;&lt;code&gt;&lt;span class="c1"&gt;// Before&lt;/span&gt;
&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="nx"&gt;error&lt;/span&gt; &lt;span class="o"&gt;&amp;amp;&amp;amp;&lt;/span&gt; &lt;span class="nx"&gt;error&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;message&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;

&lt;span class="c1"&gt;// After&lt;/span&gt;
&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="nx"&gt;error&lt;/span&gt; &lt;span class="o"&gt;&amp;amp;&amp;amp;&lt;/span&gt; &lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nt"&gt;span&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;&lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="nx"&gt;error&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;message&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="p"&gt;&amp;lt;/&lt;/span&gt;&lt;span class="nt"&gt;span&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;





&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight jsx"&gt;&lt;code&gt;&lt;span class="c1"&gt;// Before&lt;/span&gt;
&lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nt"&gt;p&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;&lt;/span&gt;Uploaded &lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="nx"&gt;count&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt; files&lt;span class="p"&gt;&amp;lt;/&lt;/span&gt;&lt;span class="nt"&gt;p&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;&lt;/span&gt;

&lt;span class="c1"&gt;// After&lt;/span&gt;
&lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nt"&gt;p&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;&amp;lt;&lt;/span&gt;&lt;span class="nt"&gt;span&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;&lt;/span&gt;Uploaded &lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="nx"&gt;count&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt; files&lt;span class="p"&gt;&amp;lt;/&lt;/span&gt;&lt;span class="nt"&gt;span&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;&amp;lt;/&lt;/span&gt;&lt;span class="nt"&gt;p&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;No visual change, no new classes. We went through every component in the QR builder, the file upload flow, the editor and the download modals, and wrapped anything that changes at runtime: status labels, counters, button text next to spinners.&lt;/p&gt;

&lt;h3&gt;
  
  
  Layer 3: &lt;code&gt;translate="no"&lt;/code&gt; — but only where it makes sense
&lt;/h3&gt;

&lt;p&gt;The tempting shortcut is &lt;code&gt;&amp;lt;html translate="no"&amp;gt;&lt;/code&gt;. Don't. Your users turned on translation because they need it — breaking the crash by breaking the translation just moves the problem.&lt;/p&gt;

&lt;p&gt;We only opt out content that &lt;strong&gt;shouldn't&lt;/strong&gt; be translated anyway:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;URL, email, phone and password inputs&lt;/li&gt;
&lt;li&gt;short links and destination URLs&lt;/li&gt;
&lt;li&gt;the QR preview and technical labels (sizes, PNG/SVG)&lt;/li&gt;
&lt;li&gt;the brand name in the header and footer
&lt;/li&gt;
&lt;/ul&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight jsx"&gt;&lt;code&gt;&lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nt"&gt;input&lt;/span&gt; &lt;span class="na"&gt;type&lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="s"&gt;"url"&lt;/span&gt; &lt;span class="na"&gt;translate&lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="s"&gt;"no"&lt;/span&gt; &lt;span class="p"&gt;/&amp;gt;&lt;/span&gt;
&lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nt"&gt;span&lt;/span&gt; &lt;span class="na"&gt;translate&lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="s"&gt;"no"&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;&lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="nx"&gt;shortUrl&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="p"&gt;&amp;lt;/&lt;/span&gt;&lt;span class="nt"&gt;span&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Everything else — headings, buttons, help text — still gets translated.&lt;/p&gt;




&lt;h2&gt;
  
  
  How we verified it
&lt;/h2&gt;

&lt;p&gt;After deploying, I ran the fake-translate script on the live site: the public PDF QR generator, then the logged-in dashboard. I switched QR types back and forth, toggled the logo option, typed into inputs, and navigated from the builder to the landing page editor and back — re-running &lt;code&gt;fakeTranslate()&lt;/code&gt; after each step.&lt;/p&gt;

&lt;p&gt;Result: zero uncaught errors, no reloads, and the UI kept updating live with the "translated" text in place. The brand name and URL fields stayed untouched, as intended.&lt;/p&gt;

&lt;p&gt;We'll keep watching the &lt;code&gt;dom_guard_*&lt;/code&gt; events in session analytics. If they show up a lot on a specific page, that page still has bare text nodes to wrap.&lt;/p&gt;




&lt;h2&gt;
  
  
  TL;DR
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;Google Translate replaces text nodes with &lt;code&gt;&amp;lt;font&amp;gt;&lt;/code&gt; elements. React still holds the old nodes and crashes on &lt;code&gt;removeChild&lt;/code&gt; / &lt;code&gt;insertBefore&lt;/code&gt;.&lt;/li&gt;
&lt;li&gt;You can reproduce it without Translate by wrapping text nodes in &lt;code&gt;&amp;lt;font&amp;gt;&lt;/code&gt; from the console.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Layer 1:&lt;/strong&gt; patch &lt;code&gt;removeChild&lt;/code&gt; / &lt;code&gt;insertBefore&lt;/code&gt; before hydration, client-only, and log when it fires.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Layer 2:&lt;/strong&gt; wrap conditional and dynamic text in &lt;code&gt;&amp;lt;span&amp;gt;&lt;/code&gt;.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Layer 3:&lt;/strong&gt; &lt;code&gt;translate="no"&lt;/code&gt; only on inputs, URLs and brand names — never on the whole app.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;The part that stuck with me: this bug hit the single most engaged user we had that month. International users who translate your app are often exactly the people working hardest to use it.&lt;/p&gt;




&lt;p&gt;&lt;em&gt;I'm building &lt;a href="https://qrflows.app" rel="noopener noreferrer"&gt;QRflows&lt;/a&gt; in public — dynamic QR codes with Smart Rules, real-time analytics and a Claude MCP integration. If you've hit this bug with a different framework, I'd like to hear how you handled it.&lt;/em&gt;&lt;/p&gt;

</description>
      <category>react</category>
      <category>javascript</category>
      <category>webdev</category>
      <category>debugging</category>
    </item>
    <item>
      <title>Why I added llms.txt to my SaaS — and what happened when Claude actually read it</title>
      <dc:creator>QRflows</dc:creator>
      <pubDate>Tue, 18 Aug 2026 08:22:17 +0000</pubDate>
      <link>https://dev.to/qrflows/why-i-added-llmstxt-to-my-saas-and-what-happened-when-claude-actually-read-it-51k4</link>
      <guid>https://dev.to/qrflows/why-i-added-llmstxt-to-my-saas-and-what-happened-when-claude-actually-read-it-51k4</guid>
      <description>&lt;p&gt;I build &lt;a href="https://qrflows.app" rel="noopener noreferrer"&gt;QRflows&lt;/a&gt; — a dynamic QR code platform. A few months ago I did two things back to back: added an MCP server so Claude can create QR codes from chat, and added an &lt;code&gt;llms.txt&lt;/code&gt; file to the root of the site.&lt;/p&gt;

&lt;p&gt;The MCP server got all the attention. The llms.txt file got none. But in some ways it was the more interesting decision — and the reasoning behind it taught me something about how AI systems actually navigate websites.&lt;/p&gt;




&lt;h2&gt;
  
  
  What llms.txt actually is
&lt;/h2&gt;

&lt;p&gt;&lt;code&gt;llms.txt&lt;/code&gt; is a plain Markdown file at the root of your domain. It tells AI systems what your site is, which pages matter, and where to find more information.&lt;/p&gt;

&lt;p&gt;Think of it as the difference between a stranger walking into your office and reading random papers off a desk versus you handing them a one-page brief first.&lt;/p&gt;

&lt;p&gt;Here's what ours looks like (simplified):&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight markdown"&gt;&lt;code&gt;&lt;span class="gh"&gt;# QRflows&lt;/span&gt;
&lt;span class="gt"&gt;
&amp;gt; Dynamic QR code platform with Smart Rules, real-time analytics, and Claude MCP integration.&lt;/span&gt;

QRflows lets you create QR codes that can be edited after printing, tracked in real time,
and routed conditionally by country, device language, or time of day.

&lt;span class="gu"&gt;## Core features&lt;/span&gt;
&lt;span class="p"&gt;
-&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="nv"&gt;Dynamic QR codes&lt;/span&gt;&lt;span class="p"&gt;](&lt;/span&gt;&lt;span class="sx"&gt;https://qrflows.app/dynamic-qr-codes&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;: Edit destination URL after printing
&lt;span class="p"&gt;-&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="nv"&gt;Smart Rules&lt;/span&gt;&lt;span class="p"&gt;](&lt;/span&gt;&lt;span class="sx"&gt;https://qrflows.app/qr-code-for-smart-rules&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;: Route by country, device, language, time
&lt;span class="p"&gt;-&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="nv"&gt;Analytics&lt;/span&gt;&lt;span class="p"&gt;](&lt;/span&gt;&lt;span class="sx"&gt;https://qrflows.app/qr-code-analytics&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;: Real-time scan tracking
&lt;span class="p"&gt;-&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="nv"&gt;Landing pages&lt;/span&gt;&lt;span class="p"&gt;](&lt;/span&gt;&lt;span class="sx"&gt;https://qrflows.app/qr-code-landing-pages&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;: Branded pages without a website
&lt;span class="p"&gt;-&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="nv"&gt;Product QR&lt;/span&gt;&lt;span class="p"&gt;](&lt;/span&gt;&lt;span class="sx"&gt;https://qrflows.app/product-qr&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;: Micro-sites for physical packaging

&lt;span class="gu"&gt;## QR code types&lt;/span&gt;
&lt;span class="p"&gt;
-&lt;/span&gt; URL, WiFi, vCard, PDF, Menu, Email, WhatsApp, Telegram, Google Review, and more

&lt;span class="gu"&gt;## Plans&lt;/span&gt;
&lt;span class="p"&gt;
-&lt;/span&gt; Starter €9/mo — 2 QR codes, 20,000 scans
&lt;span class="p"&gt;-&lt;/span&gt; Growth €19/mo — 50 QR codes, unlimited scans, landing pages, bulk creation
&lt;span class="p"&gt;-&lt;/span&gt; Pro €39/mo — 250 QR codes, A/B testing, Smart Rules, API, Product QR

&lt;span class="gu"&gt;## MCP integration&lt;/span&gt;

Claude can create, update, and track QR codes directly from chat.
Server: https://mcp.qrflows.app/mcp
Docs: https://qrflows.app/mcp

&lt;span class="gu"&gt;## Optional&lt;/span&gt;
&lt;span class="p"&gt;
-&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="nv"&gt;Blog&lt;/span&gt;&lt;span class="p"&gt;](&lt;/span&gt;&lt;span class="sx"&gt;https://qrflows.app/blog&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;: Guides and use cases
&lt;span class="p"&gt;-&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="nv"&gt;Developer API&lt;/span&gt;&lt;span class="p"&gt;](&lt;/span&gt;&lt;span class="sx"&gt;https://qrflows.app/developer-api&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;: REST API docs
&lt;span class="p"&gt;-&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="nv"&gt;Free QR generator&lt;/span&gt;&lt;span class="p"&gt;](&lt;/span&gt;&lt;span class="sx"&gt;https://qrflows.app/qr-code-generator&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;: No signup required
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;Note:&lt;/strong&gt; This is our actual file — feel free to use it as a template.&lt;/p&gt;
&lt;/blockquote&gt;




&lt;h2&gt;
  
  
  Why most explanations miss the point
&lt;/h2&gt;

&lt;p&gt;The standard explanation of llms.txt goes like this: AI models scrape your website, but HTML is full of noise — navigation, ads, scripts, footers. llms.txt gives them clean content.&lt;/p&gt;

&lt;p&gt;That's true. But it's not the most interesting part.&lt;/p&gt;

&lt;p&gt;The more interesting part is &lt;strong&gt;intent and priority&lt;/strong&gt;.&lt;/p&gt;

&lt;p&gt;When an AI model lands on your website without a guide, it has no idea which pages are canonical. Is your &lt;code&gt;/features&lt;/code&gt; page the source of truth, or &lt;code&gt;/pricing&lt;/code&gt;? Is your blog authoritative, or is it marketing content? Is that &lt;code&gt;/docs/v1&lt;/code&gt; page still current, or has it been superseded?&lt;/p&gt;

&lt;p&gt;Without guidance, the model guesses. It often guesses wrong. It might pull your pricing from a blog post that mentions old numbers, or describe your features based on a landing page written for a different audience.&lt;/p&gt;

&lt;p&gt;&lt;code&gt;llms.txt&lt;/code&gt; solves this by explicitly saying: here is what this product is, here are the pages that matter, here is the hierarchy of information.&lt;/p&gt;




&lt;h2&gt;
  
  
  The specific reason I added it alongside MCP
&lt;/h2&gt;

&lt;p&gt;Here's where it gets relevant for anyone building AI-facing tools.&lt;/p&gt;

&lt;p&gt;We have an MCP server. When someone uses Claude to create a QR code, Claude calls our tools. But before it does, Claude forms a mental model of what QRflows is — from whatever it learned during training, or from whatever it can fetch in context.&lt;/p&gt;

&lt;p&gt;If that mental model is wrong, the interaction goes sideways. Claude might not know that Smart Rules exist. It might not know that Product QR is Pro-only. It might describe our free generator as if it's our main product.&lt;/p&gt;

&lt;p&gt;&lt;code&gt;llms.txt&lt;/code&gt; is the fastest way to correct that model before any tool call happens. It's context front-loading.&lt;/p&gt;

&lt;p&gt;The sequence we want:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;User asks Claude to help with QR codes&lt;/li&gt;
&lt;li&gt;Claude reads &lt;code&gt;llms.txt&lt;/code&gt; → accurate understanding of the product&lt;/li&gt;
&lt;li&gt;Claude calls MCP tools with the right context&lt;/li&gt;
&lt;li&gt;Output is actually useful&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;Without step 2, step 4 is unreliable.&lt;/p&gt;




&lt;h2&gt;
  
  
  Who actually reads llms.txt right now
&lt;/h2&gt;

&lt;p&gt;Honest answer: it's uneven.&lt;/p&gt;

&lt;p&gt;As of mid-2026, Perplexity has confirmed support. Some versions of Claude and ChatGPT will fetch it when explicitly pointed at a domain. Cursor fetches it for library documentation. AI agents built on top of these models increasingly look for it.&lt;/p&gt;

&lt;p&gt;But most crawlers don't proactively fetch it yet. If you publish a perfect &lt;code&gt;llms.txt&lt;/code&gt; today, plenty of AI systems will still walk past it.&lt;/p&gt;

&lt;p&gt;So why bother?&lt;/p&gt;

&lt;p&gt;Two reasons.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;First, adoption is accelerating.&lt;/strong&gt; The file costs you almost nothing to publish — maybe an hour to write well. The upside is asymmetric: as more AI systems adopt the standard, every one of them gets an accurate picture of your product without you doing anything extra.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Second, it disciplines your own thinking.&lt;/strong&gt; Writing &lt;code&gt;llms.txt&lt;/code&gt; forced me to answer: what is QRflows, in one sentence? Which features are core versus secondary? What's the pricing in plain language? What pages actually matter?&lt;/p&gt;

&lt;p&gt;I've written landing pages, blog posts, and product docs for QRflows. None of them forced me to answer those questions as concisely as writing 40 lines of Markdown did.&lt;/p&gt;




&lt;h2&gt;
  
  
  What I'd do differently
&lt;/h2&gt;

&lt;p&gt;&lt;strong&gt;Write llms.txt before your landing page, not after.&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;The exercise of writing it — what is this, who is it for, what are the key pages, what's the pricing — is the same exercise as product positioning. Doing it first makes everything else easier to write.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Keep it honest about limitations.&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;I included the plan limits (Starter: 2 QR codes, Growth: 50) because if an AI recommends QRflows to someone who needs 100 QR codes on a €9 budget, that's a bad outcome for everyone. Accurate information beats optimistic information.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Add an &lt;code&gt;llms-full.txt&lt;/code&gt; if you have documentation.&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;&lt;code&gt;llms-full.txt&lt;/code&gt; is an optional companion — a single file that dumps all your documentation in one place. For a SaaS with deep docs, it's worth doing. For a simple product site, &lt;code&gt;llms.txt&lt;/code&gt; alone is enough.&lt;/p&gt;




&lt;h2&gt;
  
  
  The file itself
&lt;/h2&gt;

&lt;p&gt;Ours is live at &lt;a href="https://qrflows.app/llms.txt" rel="noopener noreferrer"&gt;qrflows.app/llms.txt&lt;/a&gt;.&lt;/p&gt;

&lt;p&gt;If you're building a SaaS and want to add one, the format is:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight markdown"&gt;&lt;code&gt;&lt;span class="gh"&gt;# Product Name&lt;/span&gt;
&lt;span class="gt"&gt;
&amp;gt; One sentence description.&lt;/span&gt;

Brief paragraph: what it is, who it's for.

&lt;span class="gu"&gt;## Section heading&lt;/span&gt;
&lt;span class="p"&gt;
-&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="nv"&gt;Page title&lt;/span&gt;&lt;span class="p"&gt;](&lt;/span&gt;&lt;span class="sx"&gt;url&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;: One-line description
&lt;span class="p"&gt;-&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="nv"&gt;Page title&lt;/span&gt;&lt;span class="p"&gt;](&lt;/span&gt;&lt;span class="sx"&gt;url&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;: One-line description

&lt;span class="gu"&gt;## Optional (lower priority content)&lt;/span&gt;
&lt;span class="p"&gt;
-&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="nv"&gt;Blog&lt;/span&gt;&lt;span class="p"&gt;](&lt;/span&gt;&lt;span class="sx"&gt;url&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;: ...
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The &lt;code&gt;&amp;gt; blockquote&lt;/code&gt; at the top is the canonical one-sentence description that AI systems extract first. Keep it sharp.&lt;/p&gt;




&lt;h2&gt;
  
  
  Links
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;Our llms.txt: &lt;a href="https://qrflows.app/llms.txt" rel="noopener noreferrer"&gt;qrflows.app/llms.txt&lt;/a&gt;
&lt;/li&gt;
&lt;li&gt;Previous post — MCP server: &lt;a href="https://dev.to/qrflows/how-i-built-an-mcp-server-so-claude-can-create-qr-codes-from-chat-205j"&gt;How I Built an MCP Server So Claude Can Create QR Codes From Chat&lt;/a&gt;
&lt;/li&gt;
&lt;li&gt;Previous post — connector directory: &lt;a href="https://dev.to/qrflows/how-to-submit-your-mcp-server-to-anthropics-connector-directory-from-someone-who-did-it-143m"&gt;How to Submit Your MCP Server to Anthropic's Connector Directory&lt;/a&gt;
&lt;/li&gt;
&lt;li&gt;Latest post: &lt;a href="https://dev.to/qrflows/google-translate-was-crashing-our-react-app-heres-how-we-fixed-removechild-for-good-32hi"&gt;Google Translate was crashing our React app — here's how we fixed "removeChild" for good&lt;/a&gt;
&lt;/li&gt;
&lt;li&gt;llms.txt spec: &lt;a href="https://llmstxt.org" rel="noopener noreferrer"&gt;llmstxt.org&lt;/a&gt;
&lt;/li&gt;
&lt;/ul&gt;




&lt;p&gt;&lt;em&gt;If you've added llms.txt to your product — drop a comment with what format you ended up using. Curious what others include in Optional.&lt;/em&gt;&lt;/p&gt;

</description>
      <category>ai</category>
      <category>saas</category>
      <category>llmstxt</category>
      <category>mcp</category>
    </item>
    <item>
      <title>How to Submit Your MCP Server to Anthropic's Connector Directory (From Someone Who Did It)</title>
      <dc:creator>QRflows</dc:creator>
      <pubDate>Wed, 03 Jun 2026 17:14:21 +0000</pubDate>
      <link>https://dev.to/qrflows/how-to-submit-your-mcp-server-to-anthropics-connector-directory-from-someone-who-did-it-143m</link>
      <guid>https://dev.to/qrflows/how-to-submit-your-mcp-server-to-anthropics-connector-directory-from-someone-who-did-it-143m</guid>
      <description>&lt;p&gt;A practical guide based on real submission experience — what the form asks, what reviewers check, and what I got wrong the first time.&lt;/p&gt;




&lt;p&gt;I built an MCP server for &lt;a href="https://qrflows.app" rel="noopener noreferrer"&gt;QRflows&lt;/a&gt; — a dynamic QR code platform. After the server was live and working, I submitted it to Anthropic's official Connector Directory. The review is ongoing, but the process taught me things I couldn't find documented anywhere in one place.&lt;/p&gt;

&lt;p&gt;This post covers the full submission process as it stands in mid-2026: what the form actually asks, the technical requirements that can silently kill your review, and how to prepare so you don't spend a week backtracking.&lt;/p&gt;




&lt;h2&gt;
  
  
  Why bother with the directory at all?
&lt;/h2&gt;

&lt;p&gt;When your MCP server is not in the directory, users have to:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;Go to Claude Settings → Connectors&lt;/li&gt;
&lt;li&gt;Click "Add custom connector"&lt;/li&gt;
&lt;li&gt;Paste your server URL manually&lt;/li&gt;
&lt;li&gt;Authenticate&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;That's four steps with a copy-paste. Most non-developer users won't do it.&lt;/p&gt;

&lt;p&gt;Once your server is in the directory, users just find you in the list, click Connect, and authorize. Same OAuth flow — but the friction disappears. The directory is also how Anthropic surfaces integrations to Claude Pro and Team users who never look at developer docs.&lt;/p&gt;

&lt;p&gt;For a SaaS product, that's a significant distribution channel.&lt;/p&gt;




&lt;h2&gt;
  
  
  What you can submit
&lt;/h2&gt;

&lt;p&gt;The directory accepts three types:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Remote MCP server&lt;/strong&gt; — an internet-hosted server with tools, resources, prompts&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Desktop extension&lt;/strong&gt; — local MCP server packaged as an &lt;code&gt;.mcpb&lt;/code&gt; bundle for Claude Desktop&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;MCP App&lt;/strong&gt; — an MCP server that renders interactive UI inside the chat (needs extra screenshots)&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;QRflows is a remote MCP server. The form and required assets differ by type, so confirm which one you're submitting before you start.&lt;/p&gt;




&lt;h2&gt;
  
  
  The submission form — what it actually asks
&lt;/h2&gt;

&lt;p&gt;The form is at &lt;code&gt;claude.com/docs/connectors/building/submission&lt;/code&gt;. It's not short.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2F0e52cwoavymqmhhhr3a8.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2F0e52cwoavymqmhhhr3a8.png" alt="Six-section overview of the Anthropic MCP Directory submission form" width="772" height="664"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Here's what each section covers so you can prepare everything before you open it:&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;1. Server basics&lt;/strong&gt; — name, tagline, server URL, connector type (remote / desktop / MCP App), primary use cases in 2–3 sentences.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;2. Connection details&lt;/strong&gt; — transport protocol (must be Streamable HTTP, SSE is no longer accepted), auth type, read/write capabilities, whether your OAuth callback URL is registered.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;3. Tools &amp;amp; resources&lt;/strong&gt; — list every tool with a human-readable title, confirm annotations are in place, confirm read and write tools are separate, confirm tool names are ≤ 64 chars.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;4. Documentation&lt;/strong&gt; — public docs URL (a single help page or blog post is enough), minimum 3 example prompts that exercise different tools, setup and auth steps. You can share a private staging link with Anthropic during review if docs aren't public yet.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;5. Privacy &amp;amp; compliance&lt;/strong&gt; — privacy policy URL, data handling summary (what you collect, how long you keep it, who you share it with), confirmation of HTTPS and Origin-header validation.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;6. Test account &amp;amp; branding&lt;/strong&gt; — login credentials for a test account with realistic sample data, server logo (URL or SVG), favicon. For MCP Apps only: 3–5 PNG screenshots at min 1000px wide.&lt;/p&gt;




&lt;h2&gt;
  
  
  The technical requirements that trip people up
&lt;/h2&gt;

&lt;h3&gt;
  
  
  1. Tool annotations — the #1 reason for rejection
&lt;/h3&gt;

&lt;p&gt;Every tool needs two things: a &lt;code&gt;title&lt;/code&gt; and the right safety hint.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2F28mhf1lhnbqdaalme7j0.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2F28mhf1lhnbqdaalme7j0.png" alt="Tool annotation examples showing readOnlyHint and destructiveHint for list, create, and delete QR tools" width="800" height="271"&gt;&lt;/a&gt;&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;&lt;span class="c1"&gt;// Read-only tool&lt;/span&gt;
&lt;span class="nx"&gt;server&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;tool&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
  &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;list_qr_codes&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;List all QR codes in the account.&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="nx"&gt;schema&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="nx"&gt;handler&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="na"&gt;title&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;List QR Codes&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="na"&gt;readOnlyHint&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kc"&gt;true&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="na"&gt;destructiveHint&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kc"&gt;false&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="p"&gt;}&lt;/span&gt;
&lt;span class="p"&gt;);&lt;/span&gt;

&lt;span class="c1"&gt;// Write tool&lt;/span&gt;
&lt;span class="nx"&gt;server&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;tool&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
  &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;create_qr&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;Create a new dynamic QR code.&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="nx"&gt;schema&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="nx"&gt;handler&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="na"&gt;title&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;Create QR Code&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="na"&gt;readOnlyHint&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kc"&gt;false&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="na"&gt;destructiveHint&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kc"&gt;false&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="p"&gt;}&lt;/span&gt;
&lt;span class="p"&gt;);&lt;/span&gt;

&lt;span class="c1"&gt;// Destructive tool — be explicit&lt;/span&gt;
&lt;span class="nx"&gt;server&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;tool&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
  &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;delete_qr&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;Permanently delete a QR code.&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="nx"&gt;schema&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="nx"&gt;handler&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="na"&gt;title&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;Delete QR Code&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="na"&gt;readOnlyHint&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kc"&gt;false&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="na"&gt;destructiveHint&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kc"&gt;true&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="p"&gt;}&lt;/span&gt;
&lt;span class="p"&gt;);&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Missing annotations reportedly cause around 30% of all directory rejections. The other mistake: don't bundle read and write into one generic tool. A tool called &lt;code&gt;api_request&lt;/code&gt; with a &lt;code&gt;method&lt;/code&gt; parameter accepting &lt;code&gt;GET&lt;/code&gt;, &lt;code&gt;POST&lt;/code&gt;, &lt;code&gt;PATCH&lt;/code&gt;, &lt;code&gt;DELETE&lt;/code&gt; will fail review. Split them.&lt;/p&gt;

&lt;h3&gt;
  
  
  2. OAuth callback URL
&lt;/h3&gt;

&lt;p&gt;For the browser-based Claude (claude.ai), register exactly this redirect URI:&lt;br&gt;
&lt;a href="https://claude.ai/api/mcp/auth_callback" rel="noopener noreferrer"&gt;https://claude.ai/api/mcp/auth_callback&lt;/a&gt;&lt;br&gt;
Claude Code uses a loopback redirect on &lt;code&gt;localhost&lt;/code&gt; with an ephemeral port. If you want to support Claude Code users, your auth server needs to accept loopback redirects with the port ignored.&lt;/p&gt;

&lt;p&gt;PKCE with &lt;code&gt;S256&lt;/code&gt; is required. Your authorization server metadata should declare:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight json"&gt;&lt;code&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"code_challenge_methods_supported"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="s2"&gt;"S256"&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Plain OAuth 2.0 without PKCE won't pass.&lt;/p&gt;

&lt;h3&gt;
  
  
  3. Origin-header validation
&lt;/h3&gt;

&lt;p&gt;Your MCP server must validate the &lt;code&gt;Origin&lt;/code&gt; header and reject requests not coming from Claude. This is a security requirement, not optional.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;&lt;span class="c1"&gt;// Cloudflare Workers example&lt;/span&gt;
&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;origin&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;request&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;headers&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;get&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;Origin&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;span class="k"&gt;if &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;origin&lt;/span&gt; &lt;span class="o"&gt;!==&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;https://claude.ai&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nc"&gt;Response&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;Forbidden&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="na"&gt;status&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="mi"&gt;403&lt;/span&gt; &lt;span class="p"&gt;});&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h3&gt;
  
  
  4. Protected Resource Metadata
&lt;/h3&gt;

&lt;p&gt;Claude uses this to discover your authorization server. Host it at &lt;code&gt;/.well-known/oauth-protected-resource&lt;/code&gt;:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight json"&gt;&lt;code&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"resource"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"https://mcp.qrflows.app"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"authorization_servers"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="s2"&gt;"https://qrflows.app"&lt;/span&gt;&lt;span class="p"&gt;],&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"scopes_supported"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="s2"&gt;"qr:read"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"qr:write"&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;If this endpoint is missing, Claude can't complete OAuth discovery and the connector won't authenticate.&lt;/p&gt;




&lt;h2&gt;
  
  
  The documentation requirement
&lt;/h2&gt;

&lt;p&gt;Anthropic wants docs that let a reviewer test your connector in 10 minutes without prior knowledge of your product.&lt;/p&gt;

&lt;p&gt;The minimum:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;What the connector does (2–3 sentences)&lt;/li&gt;
&lt;li&gt;How to connect step by step&lt;/li&gt;
&lt;li&gt;At least 3 example prompts that exercise different tools&lt;/li&gt;
&lt;li&gt;What the expected output looks like&lt;/li&gt;
&lt;li&gt;Privacy policy link&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;For QRflows I created &lt;a href="https://qrflows.app/mcp" rel="noopener noreferrer"&gt;qrflows.app/mcp&lt;/a&gt; as the dedicated docs page. A single well-organized page is enough.&lt;/p&gt;




&lt;h2&gt;
  
  
  The test account
&lt;/h2&gt;

&lt;p&gt;Create a separate account — not your main production account. Load it with realistic sample data. Write down the credentials before you open the form.&lt;/p&gt;

&lt;p&gt;For QRflows I created a test account with 6–7 QR codes of different types (URL, WiFi, vCard, Smart Rules) so reviewers could test &lt;code&gt;list_qr_codes&lt;/code&gt;, &lt;code&gt;get_qr_stats&lt;/code&gt;, and &lt;code&gt;update_qr_url&lt;/code&gt; with real data.&lt;/p&gt;

&lt;p&gt;If your tools operate on empty state, reviewers can't tell if they work or are broken.&lt;/p&gt;




&lt;h2&gt;
  
  
  Review timeline
&lt;/h2&gt;

&lt;p&gt;Anthropic is upfront: they can't promise to review or respond to every submission individually because of volume. The typical timeline they cite is ~2 weeks, but it can run longer.&lt;/p&gt;

&lt;p&gt;My submission has been in review for about a month. No response yet — from what I can tell that's within the normal range given submission volume right now.&lt;/p&gt;

&lt;p&gt;While you're waiting: your server is already usable as a custom connector. Share the URL, write about it, put it in your docs. The directory listing is a distribution multiplier, not a prerequisite.&lt;/p&gt;




&lt;h2&gt;
  
  
  What I'd do differently
&lt;/h2&gt;

&lt;p&gt;&lt;strong&gt;Read the submission guide before building.&lt;/strong&gt; Once I had the full list of requirements, I realized I was missing the Protected Resource Metadata endpoint and had to add it after the fact. Twenty minutes of reading upfront would have saved two hours.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Load the test account before submitting.&lt;/strong&gt; I initially created a test account with no data. Reviewers testing &lt;code&gt;list_qr_codes&lt;/code&gt; would have seen an empty array with no way to tell if the tool worked.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Write docs first.&lt;/strong&gt; The form asks for a public docs URL. If you don't have it ready, you have to pause. Write the docs page before you open the form.&lt;/p&gt;




&lt;h2&gt;
  
  
  Submission checklist
&lt;/h2&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fxfl2xe0z9d0gks7uw0mf.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fxfl2xe0z9d0gks7uw0mf.png" alt="Pre-submission checklist covering server, OAuth, tools, docs, test account, and branding" width="800" height="551"&gt;&lt;/a&gt;&lt;/p&gt;




&lt;h2&gt;
  
  
  Links
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;QRflows MCP server: &lt;code&gt;https://mcp.qrflows.app/mcp&lt;/code&gt;
&lt;/li&gt;
&lt;li&gt;QRflows MCP docs: &lt;a href="https://qrflows.app/mcp" rel="noopener noreferrer"&gt;qrflows.app/mcp&lt;/a&gt;
&lt;/li&gt;
&lt;li&gt;Submission guide: &lt;code&gt;claude.com/docs/connectors/building/submission&lt;/code&gt;
&lt;/li&gt;
&lt;li&gt;MCP spec: &lt;a href="https://modelcontextprotocol.io" rel="noopener noreferrer"&gt;modelcontextprotocol.io&lt;/a&gt;
&lt;/li&gt;
&lt;li&gt;My previous post: &lt;a href="https://dev.to/qrflows/how-i-built-an-mcp-server-so-claude-can-create-qr-codes-from-chat-205j"&gt;How I Built an MCP Server So Claude Can Create QR Codes From Chat&lt;/a&gt;
&lt;/li&gt;
&lt;/ul&gt;




&lt;p&gt;&lt;em&gt;I'll update this post when the review completes — approved or not. Happy to answer questions in the comments.&lt;/em&gt;&lt;/p&gt;

</description>
      <category>mcp</category>
      <category>claude</category>
      <category>ai</category>
      <category>tutorial</category>
    </item>
    <item>
      <title>How I Built an MCP Server So Claude Can Create QR Codes From Chat</title>
      <dc:creator>QRflows</dc:creator>
      <pubDate>Fri, 29 May 2026 21:11:00 +0000</pubDate>
      <link>https://dev.to/qrflows/how-i-built-an-mcp-server-so-claude-can-create-qr-codes-from-chat-205j</link>
      <guid>https://dev.to/qrflows/how-i-built-an-mcp-server-so-claude-can-create-qr-codes-from-chat-205j</guid>
      <description>&lt;p&gt;I launched a SaaS product called &lt;a href="https://qrflows.app" rel="noopener noreferrer"&gt;QRflows&lt;/a&gt; — a dynamic QR code platform. Two months in, I decided to build an MCP (Model Context Protocol) server for it. Now Claude can create, update, and track QR codes directly from a chat conversation, without touching a dashboard.&lt;/p&gt;

&lt;p&gt;This post is about why I built it, how it works technically, and what I learned along the way.&lt;/p&gt;




&lt;h2&gt;
  
  
  What is MCP and why did I care
&lt;/h2&gt;

&lt;p&gt;MCP is Anthropic's open protocol that lets AI assistants like Claude connect to external services. Think of it like a USB standard — any MCP-compatible server can plug into Claude and give it new tools.&lt;/p&gt;

&lt;p&gt;For QRflows, this meant Claude could become a QR code manager. A user types "create a QR code for my restaurant menu that routes to the breakfast version before 11am and the dinner version after" — and it just happens.&lt;/p&gt;

&lt;p&gt;That's a real use case for my product's Smart Rules feature. Before MCP, the user had to go to the dashboard, create a QR, set up routing rules manually. With MCP, the whole thing is one sentence in chat.&lt;/p&gt;

&lt;p&gt;Here's what it looks like in practice — Claude fetching stats across all QR codes for a full month:&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2F7bksp07xh9ajo52m3vx4.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2F7bksp07xh9ajo52m3vx4.png" alt="Claude fetching April stats for all 20 QR codes" width="683" height="784"&gt;&lt;/a&gt;&lt;/p&gt;




&lt;h2&gt;
  
  
  The stack
&lt;/h2&gt;

&lt;p&gt;QRflows itself runs on Laravel + React. But the MCP server is completely separate:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Runtime&lt;/strong&gt;: Cloudflare Workers (deployed with &lt;code&gt;wrangler deploy&lt;/code&gt;)&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Language&lt;/strong&gt;: TypeScript&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;MCP SDK&lt;/strong&gt;: &lt;code&gt;@modelcontextprotocol/sdk&lt;/code&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Auth&lt;/strong&gt;: OAuth 2.0 (required by Anthropic for remote MCP servers)&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Storage&lt;/strong&gt;: Cloudflare KV for OAuth token store&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;The server lives at &lt;code&gt;mcp.qrflows.app&lt;/code&gt; and communicates via HTTP (Streamable HTTP transport).&lt;/p&gt;




&lt;h2&gt;
  
  
  The tools I implemented
&lt;/h2&gt;

&lt;p&gt;The MCP server exposes 10 tools to Claude:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;create_qr          — create a new QR code (16 types supported)
update_qr_url      — change the destination URL without reprinting
update_qr          — update any QR fields
update_wifi_qr     — update WiFi credentials
list_qr_codes      — list all QR codes in the account
get_qr             — get details for a specific QR
get_qr_stats       — get scan analytics
delete_qr          — delete a QR code
apply_smart_rules  — set geo/device/time routing rules
get_account_usage  — check plan limits and usage
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Each tool has proper MCP annotations:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;&lt;span class="nx"&gt;server&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;tool&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
  &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;create_qr&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;Create a new dynamic QR code in QRflows (16 QR types).&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="na"&gt;name&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;z&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;string&lt;/span&gt;&lt;span class="p"&gt;().&lt;/span&gt;&lt;span class="nf"&gt;describe&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;Human-readable name for the QR code&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt;
    &lt;span class="na"&gt;qr_type&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;z&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;enum&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;qrTypes&lt;/span&gt;&lt;span class="p"&gt;).&lt;/span&gt;&lt;span class="k"&gt;default&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;url&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt;
    &lt;span class="na"&gt;content&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;z&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;record&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;z&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;unknown&lt;/span&gt;&lt;span class="p"&gt;()).&lt;/span&gt;&lt;span class="nf"&gt;describe&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
      &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;Payload by type, e.g. url: { url }; smart_rules: { default_url, rules: [...] }&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;
    &lt;span class="p"&gt;),&lt;/span&gt;
  &lt;span class="p"&gt;},&lt;/span&gt;
  &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="na"&gt;title&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;Create QR Code&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="na"&gt;readOnlyHint&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kc"&gt;false&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="na"&gt;destructiveHint&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kc"&gt;false&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="na"&gt;idempotentHint&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kc"&gt;false&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="na"&gt;openWorldHint&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kc"&gt;true&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="p"&gt;},&lt;/span&gt;
  &lt;span class="k"&gt;async &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;input&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;api&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nc"&gt;QrflowsApi&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;env&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;token&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="nf"&gt;toolSuccessWithUserMarkdownLeadingJson&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
      &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nx"&gt;api&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;createQr&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;input&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="p"&gt;);&lt;/span&gt;
  &lt;span class="p"&gt;}&lt;/span&gt;
&lt;span class="p"&gt;);&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The &lt;code&gt;readOnlyHint&lt;/code&gt;, &lt;code&gt;destructiveHint&lt;/code&gt;, and &lt;code&gt;idempotentHint&lt;/code&gt; annotations are important — they tell Claude how to handle each tool safely. Read-only tools like &lt;code&gt;list_qr_codes&lt;/code&gt; get &lt;code&gt;readOnlyHint: true&lt;/code&gt;. The &lt;code&gt;delete_qr&lt;/code&gt; tool gets &lt;code&gt;destructiveHint: true&lt;/code&gt; so Claude knows to be careful.&lt;/p&gt;




&lt;h2&gt;
  
  
  OAuth was the hard part
&lt;/h2&gt;

&lt;p&gt;Anthropic requires remote MCP servers to implement OAuth 2.0. This makes sense — you don't want Claude connecting to a QR service without the user explicitly authorizing it.&lt;/p&gt;

&lt;p&gt;The flow looks like this:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;User adds QRflows as a connector in Claude&lt;/li&gt;
&lt;li&gt;Claude redirects to &lt;code&gt;mcp.qrflows.app/auth/authorize&lt;/code&gt;
&lt;/li&gt;
&lt;li&gt;User logs in with their QRflows credentials&lt;/li&gt;
&lt;li&gt;OAuth token is issued and stored in Cloudflare KV&lt;/li&gt;
&lt;li&gt;All subsequent MCP tool calls use that token to hit the QRflows API&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;The trickiest part was the token refresh logic. Cloudflare Workers have no persistent state between requests, so I use KV with TTL to store tokens and refresh them automatically.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;&lt;span class="c1"&gt;// Store in KV with expiry&lt;/span&gt;
&lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nx"&gt;env&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;OAUTH_STORE&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;put&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
  &lt;span class="s2"&gt;`token:&lt;/span&gt;&lt;span class="p"&gt;${&lt;/span&gt;&lt;span class="nx"&gt;userId&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;`&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="nx"&gt;JSON&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;stringify&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt; &lt;span class="nx"&gt;access_token&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;refresh_token&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;expires_at&lt;/span&gt; &lt;span class="p"&gt;}),&lt;/span&gt;
  &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="na"&gt;expirationTtl&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="mi"&gt;3600&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt;
&lt;span class="p"&gt;);&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;






&lt;h2&gt;
  
  
  Smart Rules through chat
&lt;/h2&gt;

&lt;p&gt;The most interesting use case is Smart Rules — QRflows' geo/device/time routing feature.&lt;/p&gt;

&lt;p&gt;A user can tell Claude:&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;"Create a QR code that sends people in Spain to qrflows.app/es and everyone else to qrflows.app"&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;Claude translates this into a &lt;code&gt;create_qr&lt;/code&gt; call with &lt;code&gt;qr_type: "smart_rules"&lt;/code&gt; and the right routing config:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight json"&gt;&lt;code&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"qr_type"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"smart_rules"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"content"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="nl"&gt;"default_url"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"https://qrflows.app"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="nl"&gt;"rules"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="w"&gt;
      &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
        &lt;/span&gt;&lt;span class="nl"&gt;"condition_type"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"country"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
        &lt;/span&gt;&lt;span class="nl"&gt;"condition_value"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"ES"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
        &lt;/span&gt;&lt;span class="nl"&gt;"destination_url"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"https://qrflows.app/es"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
        &lt;/span&gt;&lt;span class="nl"&gt;"label"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"Spain"&lt;/span&gt;&lt;span class="w"&gt;
      &lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;This is where natural language + structured API really clicks. Describing routing rules in JSON is annoying. Describing them in English is natural. Claude handles the translation.&lt;/p&gt;

&lt;p&gt;And when you ask for stats on a specific QR, Claude returns a full analytics breakdown inline:&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2F0elligra6py5bfen6q4r.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2F0elligra6py5bfen6q4r.png" alt="Claude showing scan stats for a specific QR code with chart" width="678" height="606"&gt;&lt;/a&gt;&lt;/p&gt;




&lt;h2&gt;
  
  
  Response format: markdown first
&lt;/h2&gt;

&lt;p&gt;One thing I got wrong initially: my tools returned raw JSON. Claude would display it as a code block and the conversation felt clunky.&lt;/p&gt;

&lt;p&gt;The fix was to make tools return a &lt;strong&gt;markdown block first&lt;/strong&gt;, then the JSON for Claude's internal use, separated by a delimiter:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight markdown"&gt;&lt;code&gt;Here's your QR code for the restaurant menu:

&lt;span class="p"&gt;![&lt;/span&gt;&lt;span class="nv"&gt;QR Code&lt;/span&gt;&lt;span class="p"&gt;](&lt;/span&gt;&lt;span class="sx"&gt;https://qrflows.app/qr/abc123.png&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;

&lt;span class="gs"&gt;**Download:**&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="nv"&gt;PNG&lt;/span&gt;&lt;span class="p"&gt;](&lt;/span&gt;&lt;span class="sx"&gt;https://...&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; | &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="nv"&gt;SVG&lt;/span&gt;&lt;span class="p"&gt;](&lt;/span&gt;&lt;span class="sx"&gt;https://...&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="gs"&gt;**Destination:**&lt;/span&gt; https://your-menu.com
&lt;span class="gs"&gt;**Type:**&lt;/span&gt; Menu QR
&lt;span class="p"&gt;
---&lt;/span&gt;
qrflows_tool_json
{ ... raw json ... }
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Claude surfaces the markdown naturally in conversation. The JSON after the delimiter is parsed by Claude if it needs to chain tool calls.&lt;/p&gt;

&lt;p&gt;When listing all QR codes, Claude also renders a full breakdown table automatically:&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2F9vivpxlts73kyffpi710.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2F9vivpxlts73kyffpi710.png" alt="Claude rendering a full table of all QR codes with scan counts" width="692" height="880"&gt;&lt;/a&gt;&lt;/p&gt;




&lt;h2&gt;
  
  
  How to connect it right now
&lt;/h2&gt;

&lt;p&gt;The MCP server is live at &lt;code&gt;https://mcp.qrflows.app/mcp&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;To connect in Claude (claude.ai):&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;Go to &lt;strong&gt;Settings → Connectors&lt;/strong&gt;
&lt;/li&gt;
&lt;li&gt;Click &lt;strong&gt;Add custom connector&lt;/strong&gt;
&lt;/li&gt;
&lt;li&gt;Enter &lt;code&gt;https://mcp.qrflows.app/mcp&lt;/code&gt;
&lt;/li&gt;
&lt;li&gt;Authorize with your QRflows account&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;Then try:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Create a WiFi QR code for my office. SSID: OfficeWifi, password: correct-horse-battery
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;





&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Show me scan stats for all my QR codes from last week
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;





&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Update the URL on my "Menu QR" to point to https://myrestaurant.com/menu-v2
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;






&lt;h2&gt;
  
  
  Submitting to Anthropic's connector directory
&lt;/h2&gt;

&lt;p&gt;I've submitted QRflows to Anthropic's official MCP connector directory. The review is ongoing — once approved, QRflows will appear in the built-in connector list inside Claude without users needing to add a custom URL.&lt;/p&gt;

&lt;p&gt;If you're building an MCP server and wondering about the submission process: there's a Google Form linked from the Anthropic docs. The technical requirements include Streamable HTTP transport, OAuth 2.0, and proper tool annotations. The review takes a few weeks.&lt;/p&gt;




&lt;h2&gt;
  
  
  What I'd do differently
&lt;/h2&gt;

&lt;p&gt;&lt;strong&gt;Start with OAuth earlier.&lt;/strong&gt; I built all 10 tools first, then retrofitted OAuth. That was backwards. OAuth shapes your entire architecture — do it first.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Test with real Claude conversations, not just the MCP inspector.&lt;/strong&gt; The inspector tells you if tools work. Only real conversations tell you if Claude uses them correctly. Claude sometimes misinterprets tool descriptions in ways the inspector won't catch.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Keep tool descriptions conversational.&lt;/strong&gt; I initially wrote terse, developer-style descriptions. Claude is better at using tools described the way you'd explain them to a colleague.&lt;/p&gt;




&lt;h2&gt;
  
  
  Links
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;QRflows: &lt;a href="https://qrflows.app" rel="noopener noreferrer"&gt;qrflows.app&lt;/a&gt;
&lt;/li&gt;
&lt;li&gt;MCP server endpoint: &lt;code&gt;https://mcp.qrflows.app/mcp&lt;/code&gt;
&lt;/li&gt;
&lt;li&gt;MCP docs: &lt;a href="https://qrflows.app/mcp" rel="noopener noreferrer"&gt;qrflows.app/mcp&lt;/a&gt;
&lt;/li&gt;
&lt;li&gt;Developer API: &lt;a href="https://qrflows.app/developer-api" rel="noopener noreferrer"&gt;qrflows.app/developer-api&lt;/a&gt;
&lt;/li&gt;
&lt;li&gt;MCP spec: &lt;a href="https://modelcontextprotocol.io" rel="noopener noreferrer"&gt;modelcontextprotocol.io&lt;/a&gt;
&lt;/li&gt;
&lt;/ul&gt;




&lt;p&gt;&lt;em&gt;Built this in about 2 weeks alongside the main product. Happy to answer questions about the implementation — drop them in the comments.&lt;/em&gt;&lt;/p&gt;

</description>
      <category>mcp</category>
      <category>claude</category>
      <category>ai</category>
      <category>tutorial</category>
    </item>
    <item>
      <title>From idea to indexed: how I launched a SaaS in 60 days with Laravel + React</title>
      <dc:creator>QRflows</dc:creator>
      <pubDate>Sat, 23 May 2026 16:16:52 +0000</pubDate>
      <link>https://dev.to/qrflows/from-idea-to-indexed-how-i-launched-a-saas-in-60-days-with-laravel-react-3gje</link>
      <guid>https://dev.to/qrflows/from-idea-to-indexed-how-i-launched-a-saas-in-60-days-with-laravel-react-3gje</guid>
      <description>&lt;p&gt;60 days from first commit to Google indexing. Here's what I built, &lt;br&gt;
what stack I chose, and what actually slowed me down.&lt;/p&gt;

&lt;h2&gt;
  
  
  What I built
&lt;/h2&gt;

&lt;p&gt;QRflows is a dynamic QR code platform. The core idea: QR codes you &lt;br&gt;
can edit after printing, with real-time scan analytics, Smart Rules &lt;br&gt;
routing, and landing pages — all in one dashboard.&lt;/p&gt;

&lt;p&gt;Sounds simple. The execution was not.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2F4wswt4zmarx5ssydaw82.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2F4wswt4zmarx5ssydaw82.png" alt=" " width="800" height="667"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  Why Laravel + React
&lt;/h2&gt;

&lt;p&gt;I needed something that could scale without becoming a mess at &lt;br&gt;
50k users, but also ship fast enough that I'd still be alive by &lt;br&gt;
launch day.&lt;/p&gt;

&lt;p&gt;Laravel handles the backend — API, auth, queue jobs, database. &lt;br&gt;
React handles the frontend — the dashboard, QR builder, analytics &lt;br&gt;
views. They communicate through a clean REST API.&lt;/p&gt;

&lt;p&gt;This separation matters. When the frontend needs to change, the &lt;br&gt;
backend doesn't care. When the backend logic gets complex, the &lt;br&gt;
frontend stays simple. Scalability isn't just about servers — it's &lt;br&gt;
about not painting yourself into a corner architecturally.&lt;/p&gt;

&lt;h2&gt;
  
  
  The hardest part
&lt;/h2&gt;

&lt;p&gt;Getting the UX and architecture to work together without &lt;br&gt;
compromising either one.&lt;/p&gt;

&lt;p&gt;It's easy to build something scalable that's painful to use. It's &lt;br&gt;
easy to build something beautiful that falls apart under load. &lt;br&gt;
Getting both right at the same time — that's where the 60 days went.&lt;/p&gt;

&lt;p&gt;Specific example: the QR builder. It needed to feel instant and &lt;br&gt;
intuitive for non-technical users, but under the hood it had to &lt;br&gt;
handle 16 different QR types, each with different validation rules, &lt;br&gt;
different output formats, and different downstream jobs.&lt;/p&gt;

&lt;p&gt;The solution was a clean abstraction layer in Laravel — each QR type &lt;br&gt;
is its own class with a shared interface — combined with a React &lt;br&gt;
component that adapts its form fields dynamically based on the &lt;br&gt;
selected type. No spaghetti. No special cases everywhere.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fyaqqrvrmxncxilyw7v8r.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fyaqqrvrmxncxilyw7v8r.png" alt=" " width="800" height="738"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  Analytics that actually tell you something
&lt;/h2&gt;

&lt;p&gt;Most QR tools show you a scan count. That's it. &lt;/p&gt;

&lt;p&gt;I wanted analytics that suggest what to do next — not just what &lt;br&gt;
happened. So the dashboard shows engagement score, scan consistency, &lt;br&gt;
peak hours, and actionable recommendations.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fpmu1fhborxdk1q86mtho.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fpmu1fhborxdk1q86mtho.png" alt=" " width="800" height="597"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  One feature I didn't plan but users needed
&lt;/h2&gt;

&lt;p&gt;A/B testing for QR codes. One printed code, multiple destinations, &lt;br&gt;
weighted traffic split. &lt;/p&gt;

&lt;p&gt;Turned out marketing teams using physical materials needed this badly &lt;br&gt;
— they couldn't run experiments without reprinting everything.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fuh1jx6m8hjxvzrbx3k66.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fuh1jx6m8hjxvzrbx3k66.png" alt=" " width="800" height="860"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  What I'd do differently
&lt;/h2&gt;

&lt;p&gt;Start with fewer QR types. I launched with 16. I should have launched &lt;br&gt;
with 5 and added the rest based on what users actually needed.&lt;/p&gt;

&lt;p&gt;Premature completeness is just as dangerous as premature optimization.&lt;/p&gt;

&lt;h2&gt;
  
  
  Where it is now
&lt;/h2&gt;

&lt;p&gt;QRflows is live at &lt;a href="https://qrflows.app" rel="noopener noreferrer"&gt;qrflows.app&lt;/a&gt;. Free trial, &lt;br&gt;
no credit card. Still early — but indexed, shipping, and getting &lt;br&gt;
first users.&lt;/p&gt;

&lt;p&gt;If you're building something similar with Laravel + React I'm happy &lt;br&gt;
to compare notes. What's your biggest architectural headache right now?&lt;/p&gt;

</description>
      <category>laravel</category>
      <category>react</category>
      <category>webdev</category>
      <category>beginners</category>
    </item>
  </channel>
</rss>
