<?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: AURA GEO</title>
    <description>The latest articles on DEV Community by AURA GEO (@aurageo).</description>
    <link>https://dev.to/aurageo</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%2F4196619%2Fe7b34d26-e0f1-4ff2-b821-5f8d51327934.png</url>
      <title>DEV Community: AURA GEO</title>
      <link>https://dev.to/aurageo</link>
    </image>
    <atom:link rel="self" type="application/rss+xml" href="https://dev.to/feed/aurageo"/>
    <language>en</language>
    <item>
      <title>Structured Data That Matches the Page: Check Your JSON-LD Against Visible Content with Python</title>
      <dc:creator>AURA GEO</dc:creator>
      <pubDate>Sun, 25 Oct 2026 17:14:03 +0000</pubDate>
      <link>https://dev.to/aurageo/structured-data-that-matches-the-page-check-your-json-ld-against-visible-content-with-python-l1e</link>
      <guid>https://dev.to/aurageo/structured-data-that-matches-the-page-check-your-json-ld-against-visible-content-with-python-l1e</guid>
      <description>&lt;p&gt;Structured data is a promise about the page it sits on. A JSON-LD block that says your app costs 12 USD, that support is at a certain address and that you're the same company as a given GitHub account is a set of claims. Search engines and AI assistants can only use those claims safely if the page backs them up.&lt;/p&gt;

&lt;p&gt;Google is explicit about this. Its &lt;a href="https://developers.google.com/search/docs/appearance/structured-data/sd-policies" rel="noopener noreferrer"&gt;general structured data guidelines&lt;/a&gt; say: "Don't mark up content that is not visible to readers of the page. For example, if the JSON-LD markup describes a performer, the HTML body must describe that same performer." The same page adds that "your structured data must be a true representation of the page content."&lt;/p&gt;

&lt;p&gt;Most validators don't check that part. They check syntax and required properties. This post shows how to write &lt;code&gt;Organization&lt;/code&gt; and &lt;code&gt;SoftwareApplication&lt;/code&gt; JSON-LD from facts that are already on the page, and how to catch drift between the two with about 90 lines of standard-library Python.&lt;/p&gt;

&lt;h2&gt;
  
  
  Start from the visible facts, not the schema
&lt;/h2&gt;

&lt;p&gt;The usual order goes wrong in a predictable way. Someone opens schema.org, fills in every property that looks relevant, and ends up with markup that says more than the page does. An email address that only exists in the JSON-LD. A price from last quarter. A &lt;code&gt;sameAs&lt;/code&gt; link to a profile nobody maintains.&lt;/p&gt;

&lt;p&gt;Go the other way. List the facts the page actually shows a visitor, then mark up only those:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;the organization or product name, exactly as written in the heading or logo text&lt;/li&gt;
&lt;li&gt;the canonical URL of the site&lt;/li&gt;
&lt;li&gt;prices with currency, as shown in the pricing table&lt;/li&gt;
&lt;li&gt;a support email or phone number, if the page shows one&lt;/li&gt;
&lt;li&gt;profile links that appear on the page (often in the footer)&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;If a fact isn't on the page and you want it in the markup, put it on the page first.&lt;/p&gt;

&lt;h2&gt;
  
  
  Organization: who you are
&lt;/h2&gt;

&lt;p&gt;Google's &lt;a href="https://developers.google.com/search/docs/appearance/structured-data/organization" rel="noopener noreferrer"&gt;Organization documentation&lt;/a&gt; says there are no required properties and recommends adding the ones that apply. It suggests placing the markup on the home page "or a single page that describes your organization, for example the about us page," and notes you "don't need to include it on every page of your site." For &lt;code&gt;name&lt;/code&gt;, it says to "use the same &lt;code&gt;name&lt;/code&gt; and &lt;code&gt;alternateName&lt;/code&gt; that you're using for your site name."&lt;/p&gt;

&lt;p&gt;&lt;code&gt;sameAs&lt;/code&gt; is described as "the URL of a page on another website with additional information about your organization", for example a social or review profile. That's the property assistants and search engines can use to connect your site to your profiles elsewhere, so it should only list profiles that are really yours and still active.&lt;/p&gt;

&lt;p&gt;A minimal block for a small SaaS company:&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;"@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;"Organization"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"@id"&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://example.com/#org"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"name"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"Example Labs"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"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://example.com/"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"email"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"support@example.com"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"sameAs"&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="s2"&gt;"https://github.com/example-labs"&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://x.com/examplelabs"&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;h2&gt;
  
  
  SoftwareApplication: what you sell
&lt;/h2&gt;

&lt;p&gt;For apps, Google's &lt;a href="https://developers.google.com/search/docs/appearance/structured-data/software-app" rel="noopener noreferrer"&gt;Software App documentation&lt;/a&gt; lists three required properties for the rich result: &lt;code&gt;name&lt;/code&gt;, &lt;code&gt;offers.price&lt;/code&gt;, and a rating or review (&lt;code&gt;aggregateRating&lt;/code&gt; or &lt;code&gt;review&lt;/code&gt;). If the app is free, it says to set &lt;code&gt;offers.price&lt;/code&gt; to &lt;code&gt;0&lt;/code&gt;. If the price is above zero, it recommends adding &lt;code&gt;offers.priceCurrency&lt;/code&gt;. &lt;code&gt;applicationCategory&lt;/code&gt; (for example &lt;code&gt;BusinessApplication&lt;/code&gt;) and &lt;code&gt;operatingSystem&lt;/code&gt; are recommended.&lt;/p&gt;

&lt;p&gt;Be careful with the rating requirement. Don't invent an &lt;code&gt;aggregateRating&lt;/code&gt; to qualify for stars. The general guidelines specifically warn against marking up "fake reviews," and the &lt;a href="https://developers.google.com/search/docs/appearance/structured-data/review-snippet" rel="noopener noreferrer"&gt;review snippet guidelines&lt;/a&gt; say that when an entity controls the reviews about itself, its pages using Organization-type markup are ineligible for star reviews. If you have no genuine, visible ratings, leave the property out. The markup is still valid schema.org and still describes your product. It just won't be eligible for that particular rich result.&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;"@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;"SoftwareApplication"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"name"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"Example Notes"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"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://example.com/"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"applicationCategory"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"BusinessApplication"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"operatingSystem"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"Web"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"publisher"&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;"@id"&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://example.com/#org"&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;"offers"&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;"@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;"Offer"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nl"&gt;"price"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mf"&gt;12.0&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nl"&gt;"priceCurrency"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"USD"&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;The &lt;code&gt;publisher&lt;/code&gt; reference uses the &lt;code&gt;@id&lt;/code&gt; from the Organization node. Putting both nodes in one &lt;code&gt;@graph&lt;/code&gt; keeps a single source of truth for the company and avoids repeating it per page.&lt;/p&gt;

&lt;p&gt;If you sell a physical product, Google's Product and merchant listing docs apply instead, with their own required fields. The checking approach below works the same way.&lt;/p&gt;

&lt;h2&gt;
  
  
  The full block, in the page head
&lt;/h2&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight html"&gt;&lt;code&gt;&lt;span class="nt"&gt;&amp;lt;script &lt;/span&gt;&lt;span class="na"&gt;type=&lt;/span&gt;&lt;span class="s"&gt;"application/ld+json"&lt;/span&gt;&lt;span class="nt"&gt;&amp;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;@context&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;https://schema.org&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;@graph&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="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;@type&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;Organization&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;@id&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;https://example.com/#org&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;name&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;Example Labs&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;url&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;https://example.com/&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;email&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;support@example.com&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;sameAs&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="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;https://github.com/example-labs&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;https://x.com/examplelabs&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="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;@type&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;SoftwareApplication&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;name&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;Example Notes&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;url&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;https://example.com/&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;applicationCategory&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;BusinessApplication&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;operatingSystem&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;Web&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;publisher&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="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;@id&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;https://example.com/#org&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;offers&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="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;@type&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;Offer&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;price&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="mf"&gt;12.0&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;priceCurrency&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;USD&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="p"&gt;}&lt;/span&gt;
&lt;span class="nt"&gt;&amp;lt;/script&amp;gt;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;And the visible part of the page, which is where things drift:&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="nt"&gt;&amp;lt;h1&amp;gt;&lt;/span&gt;Example Notes&lt;span class="nt"&gt;&amp;lt;/h1&amp;gt;&lt;/span&gt;
&lt;span class="nt"&gt;&amp;lt;p&amp;gt;&lt;/span&gt;Shared notes for small teams, made by Example Labs.&lt;span class="nt"&gt;&amp;lt;/p&amp;gt;&lt;/span&gt;
&lt;span class="nt"&gt;&amp;lt;p&amp;gt;&lt;/span&gt;Team plan: 12 USD per user per month.&lt;span class="nt"&gt;&amp;lt;/p&amp;gt;&lt;/span&gt;
&lt;span class="nt"&gt;&amp;lt;footer&amp;gt;&lt;/span&gt;
  &lt;span class="nt"&gt;&amp;lt;a&lt;/span&gt; &lt;span class="na"&gt;href=&lt;/span&gt;&lt;span class="s"&gt;"https://github.com/example-labs"&lt;/span&gt;&lt;span class="nt"&gt;&amp;gt;&lt;/span&gt;GitHub&lt;span class="nt"&gt;&amp;lt;/a&amp;gt;&lt;/span&gt; Â·
  &lt;span class="nt"&gt;&amp;lt;a&lt;/span&gt; &lt;span class="na"&gt;href=&lt;/span&gt;&lt;span class="s"&gt;"/"&lt;/span&gt;&lt;span class="nt"&gt;&amp;gt;&lt;/span&gt;Home&lt;span class="nt"&gt;&amp;lt;/a&amp;gt;&lt;/span&gt;
&lt;span class="nt"&gt;&amp;lt;/footer&amp;gt;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Two claims in the markup have no visible counterpart: the support email and the X profile. Neither is a syntax error, so a validator won't flag them.&lt;/p&gt;

&lt;h2&gt;
  
  
  A small checker: does the markup match the page?
&lt;/h2&gt;

&lt;p&gt;The script below fetches a page (or reads a saved HTML file), pulls out every &lt;code&gt;application/ld+json&lt;/code&gt; block, and walks the JSON for a set of fact-like keys: names, URLs, &lt;code&gt;sameAs&lt;/code&gt;, email, phone, price, currency and address parts. Then it checks each value against the page:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;text facts must appear in the visible text, meaning anything outside &lt;code&gt;&amp;lt;script&amp;gt;&lt;/code&gt;, &lt;code&gt;&amp;lt;style&amp;gt;&lt;/code&gt;, &lt;code&gt;&amp;lt;noscript&amp;gt;&lt;/code&gt;, &lt;code&gt;&amp;lt;template&amp;gt;&lt;/code&gt;, &lt;code&gt;&amp;lt;svg&amp;gt;&lt;/code&gt; and elements marked &lt;code&gt;hidden&lt;/code&gt; or &lt;code&gt;aria-hidden="true"&lt;/code&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;url&lt;/code&gt; and &lt;code&gt;sameAs&lt;/code&gt; values must appear as a link (&lt;code&gt;&amp;lt;a href&amp;gt;&lt;/code&gt;) on the page&lt;/li&gt;
&lt;li&gt;missing &lt;code&gt;sameAs&lt;/code&gt; profiles produce a WARN rather than a failure, since profile links are something to review, not always an error&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;It only uses the Python standard library.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="c1"&gt;#!/usr/bin/env python3
&lt;/span&gt;&lt;span class="sh"&gt;"""&lt;/span&gt;&lt;span class="s"&gt;Check that JSON-LD facts on a page also appear in its visible HTML.&lt;/span&gt;&lt;span class="sh"&gt;"""&lt;/span&gt;
&lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;json&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;re&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;sys&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;urllib&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;request&lt;/span&gt;
&lt;span class="kn"&gt;from&lt;/span&gt; &lt;span class="n"&gt;html.parser&lt;/span&gt; &lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;HTMLParser&lt;/span&gt;
&lt;span class="kn"&gt;from&lt;/span&gt; &lt;span class="n"&gt;urllib.parse&lt;/span&gt; &lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;urljoin&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;urlsplit&lt;/span&gt;

&lt;span class="n"&gt;CHECK_KEYS&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;name&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;legalName&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;alternateName&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;url&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;sameAs&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;email&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
              &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;telephone&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;price&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;priceCurrency&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;streetAddress&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
              &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;addressLocality&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;postalCode&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;
&lt;span class="n"&gt;SKIP_TAGS&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;script&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;style&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;noscript&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;template&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;svg&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;

&lt;span class="k"&gt;class&lt;/span&gt; &lt;span class="nc"&gt;Page&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;HTMLParser&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
    &lt;span class="n"&gt;VOID&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;area&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;base&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;br&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;col&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;embed&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;hr&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;img&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;input&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
            &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;link&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;meta&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;source&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;track&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;wbr&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;
    &lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;__init__&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;self&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;base&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
        &lt;span class="nf"&gt;super&lt;/span&gt;&lt;span class="p"&gt;().&lt;/span&gt;&lt;span class="nf"&gt;__init__&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;convert_charrefs&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="bp"&gt;True&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
        &lt;span class="n"&gt;self&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;base&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;self&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;stack&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;self&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;in_ld&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;base&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;[],&lt;/span&gt; &lt;span class="bp"&gt;False&lt;/span&gt;
        &lt;span class="n"&gt;self&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;text&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;self&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;links&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;self&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;ld&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;[],&lt;/span&gt; &lt;span class="nf"&gt;set&lt;/span&gt;&lt;span class="p"&gt;(),&lt;/span&gt; &lt;span class="p"&gt;[]&lt;/span&gt;
    &lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;handle_starttag&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;self&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;tag&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;attrs&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
        &lt;span class="n"&gt;a&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;dict&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;attrs&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
        &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;tag&lt;/span&gt; &lt;span class="o"&gt;==&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;a&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt; &lt;span class="ow"&gt;and&lt;/span&gt; &lt;span class="n"&gt;a&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="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;href&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
            &lt;span class="n"&gt;self&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;links&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;add&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nf"&gt;norm&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nf"&gt;urljoin&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;self&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;base&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;a&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;href&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;])))&lt;/span&gt;
        &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;tag&lt;/span&gt; &lt;span class="o"&gt;==&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;script&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt; &lt;span class="ow"&gt;and&lt;/span&gt; &lt;span class="n"&gt;a&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="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;type&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sh"&gt;""&lt;/span&gt;&lt;span class="p"&gt;).&lt;/span&gt;&lt;span class="nf"&gt;lower&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="o"&gt;==&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;application/ld+json&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
            &lt;span class="n"&gt;self&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;in_ld&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="bp"&gt;True&lt;/span&gt;
            &lt;span class="n"&gt;self&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;ld&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;append&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;""&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
        &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;tag&lt;/span&gt; &lt;span class="ow"&gt;in&lt;/span&gt; &lt;span class="n"&gt;self&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;VOID&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
            &lt;span class="k"&gt;return&lt;/span&gt;
        &lt;span class="n"&gt;hidden&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;tag&lt;/span&gt; &lt;span class="ow"&gt;in&lt;/span&gt; &lt;span class="n"&gt;SKIP_TAGS&lt;/span&gt; &lt;span class="ow"&gt;or&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;hidden&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt; &lt;span class="ow"&gt;in&lt;/span&gt; &lt;span class="n"&gt;a&lt;/span&gt; &lt;span class="ow"&gt;or&lt;/span&gt; &lt;span class="n"&gt;a&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="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;aria-hidden&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;==&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;true&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;
        &lt;span class="n"&gt;self&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;stack&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;append&lt;/span&gt;&lt;span class="p"&gt;((&lt;/span&gt;&lt;span class="n"&gt;tag&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;hidden&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt;
    &lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;handle_startendtag&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;self&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;tag&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;attrs&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
        &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;tag&lt;/span&gt; &lt;span class="o"&gt;==&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;a&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
            &lt;span class="n"&gt;self&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;handle_starttag&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;tag&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;attrs&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;   &lt;span class="c1"&gt;# records the link
&lt;/span&gt;            &lt;span class="n"&gt;self&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;handle_endtag&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;tag&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;handle_endtag&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;self&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;tag&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
        &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;tag&lt;/span&gt; &lt;span class="o"&gt;==&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;script&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
            &lt;span class="n"&gt;self&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;in_ld&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="bp"&gt;False&lt;/span&gt;
        &lt;span class="k"&gt;for&lt;/span&gt; &lt;span class="n"&gt;i&lt;/span&gt; &lt;span class="ow"&gt;in&lt;/span&gt; &lt;span class="nf"&gt;range&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nf"&gt;len&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;self&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;stack&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;-&lt;/span&gt; &lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="o"&gt;-&lt;/span&gt;&lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="o"&gt;-&lt;/span&gt;&lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;   &lt;span class="c1"&gt;# pop to the matching tag
&lt;/span&gt;            &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;self&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;stack&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="n"&gt;i&lt;/span&gt;&lt;span class="p"&gt;][&lt;/span&gt;&lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="o"&gt;==&lt;/span&gt; &lt;span class="n"&gt;tag&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
                &lt;span class="k"&gt;del&lt;/span&gt; &lt;span class="n"&gt;self&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;stack&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="n"&gt;i&lt;/span&gt;&lt;span class="p"&gt;:]&lt;/span&gt;
                &lt;span class="k"&gt;break&lt;/span&gt;
    &lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;handle_data&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;self&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;data&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
        &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;self&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;in_ld&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
            &lt;span class="n"&gt;self&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;ld&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="o"&gt;-&lt;/span&gt;&lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="o"&gt;+=&lt;/span&gt; &lt;span class="n"&gt;data&lt;/span&gt;
        &lt;span class="k"&gt;elif&lt;/span&gt; &lt;span class="ow"&gt;not&lt;/span&gt; &lt;span class="nf"&gt;any&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;h&lt;/span&gt; &lt;span class="k"&gt;for&lt;/span&gt; &lt;span class="n"&gt;_&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;h&lt;/span&gt; &lt;span class="ow"&gt;in&lt;/span&gt; &lt;span class="n"&gt;self&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;stack&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
            &lt;span class="n"&gt;self&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;text&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;append&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;data&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;

&lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;norm&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;u&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
    &lt;span class="n"&gt;p&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;urlsplit&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;u&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;strip&lt;/span&gt;&lt;span class="p"&gt;())&lt;/span&gt;
    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="sa"&gt;f&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="n"&gt;p&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;netloc&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;lower&lt;/span&gt;&lt;span class="p"&gt;().&lt;/span&gt;&lt;span class="nf"&gt;removeprefix&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt;www.&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="si"&gt;}{&lt;/span&gt;&lt;span class="n"&gt;p&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;path&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;rstrip&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt;/&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;

&lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;walk&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;node&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;path&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="sh"&gt;""&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
    &lt;span class="sh"&gt;"""&lt;/span&gt;&lt;span class="s"&gt;Yield (path, key, value) for every scalar we want to check.&lt;/span&gt;&lt;span class="sh"&gt;"""&lt;/span&gt;
    &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="nf"&gt;isinstance&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;node&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nb"&gt;dict&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
        &lt;span class="k"&gt;for&lt;/span&gt; &lt;span class="n"&gt;k&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;v&lt;/span&gt; &lt;span class="ow"&gt;in&lt;/span&gt; &lt;span class="n"&gt;node&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;items&lt;/span&gt;&lt;span class="p"&gt;():&lt;/span&gt;
            &lt;span class="k"&gt;yield&lt;/span&gt; &lt;span class="k"&gt;from&lt;/span&gt; &lt;span class="nf"&gt;walk&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;v&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sa"&gt;f&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="n"&gt;path&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="s"&gt;.&lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="n"&gt;k&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt; &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;path&lt;/span&gt; &lt;span class="k"&gt;else&lt;/span&gt; &lt;span class="n"&gt;k&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="k"&gt;elif&lt;/span&gt; &lt;span class="nf"&gt;isinstance&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;node&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nb"&gt;list&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
        &lt;span class="k"&gt;for&lt;/span&gt; &lt;span class="n"&gt;i&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;v&lt;/span&gt; &lt;span class="ow"&gt;in&lt;/span&gt; &lt;span class="nf"&gt;enumerate&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;node&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
            &lt;span class="k"&gt;yield&lt;/span&gt; &lt;span class="k"&gt;from&lt;/span&gt; &lt;span class="nf"&gt;walk&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;v&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sa"&gt;f&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="n"&gt;path&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="s"&gt;[&lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="n"&gt;i&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="s"&gt;]&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="k"&gt;elif&lt;/span&gt; &lt;span class="nf"&gt;isinstance&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;node&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nb"&gt;str&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nb"&gt;int&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nb"&gt;float&lt;/span&gt;&lt;span class="p"&gt;)):&lt;/span&gt;
        &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="nf"&gt;isinstance&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;node&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nb"&gt;float&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="ow"&gt;and&lt;/span&gt; &lt;span class="n"&gt;node&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;is_integer&lt;/span&gt;&lt;span class="p"&gt;():&lt;/span&gt;
            &lt;span class="n"&gt;node&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;int&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;node&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;                  &lt;span class="c1"&gt;# 19.0 -&amp;gt; "19"
&lt;/span&gt;        &lt;span class="n"&gt;key&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;re&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;sub&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sa"&gt;r&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;\[\d+\]$&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sh"&gt;""&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;path&lt;/span&gt;&lt;span class="p"&gt;).&lt;/span&gt;&lt;span class="nf"&gt;split&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;.&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)[&lt;/span&gt;&lt;span class="o"&gt;-&lt;/span&gt;&lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;
        &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;key&lt;/span&gt; &lt;span class="ow"&gt;in&lt;/span&gt; &lt;span class="n"&gt;CHECK_KEYS&lt;/span&gt; &lt;span class="ow"&gt;and&lt;/span&gt; &lt;span class="ow"&gt;not&lt;/span&gt; &lt;span class="n"&gt;re&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;search&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sa"&gt;r&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;(logo|image)&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;path&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
            &lt;span class="k"&gt;yield&lt;/span&gt; &lt;span class="n"&gt;path&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;key&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nf"&gt;str&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;node&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;

&lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;main&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;url&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;saved&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="bp"&gt;None&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
    &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;saved&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;                                 &lt;span class="c1"&gt;# test a local build before deploy
&lt;/span&gt;        &lt;span class="n"&gt;html&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;open&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;saved&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;encoding&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;utf-8&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;).&lt;/span&gt;&lt;span class="nf"&gt;read&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
    &lt;span class="k"&gt;else&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
        &lt;span class="n"&gt;req&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;urllib&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;request&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nc"&gt;Request&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;url&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;headers&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;User-Agent&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;ld-check/0.1&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;})&lt;/span&gt;
        &lt;span class="n"&gt;html&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;urllib&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;request&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;urlopen&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;req&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;timeout&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="mi"&gt;20&lt;/span&gt;&lt;span class="p"&gt;).&lt;/span&gt;&lt;span class="nf"&gt;read&lt;/span&gt;&lt;span class="p"&gt;().&lt;/span&gt;&lt;span class="nf"&gt;decode&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;utf-8&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;replace&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="n"&gt;page&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nc"&gt;Page&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;url&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="n"&gt;page&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;feed&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;html&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="n"&gt;text&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;re&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;sub&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sa"&gt;r&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;\s+&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt; &lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt; &lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;join&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;page&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;text&lt;/span&gt;&lt;span class="p"&gt;)).&lt;/span&gt;&lt;span class="nf"&gt;lower&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
    &lt;span class="n"&gt;problems&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="mi"&gt;0&lt;/span&gt;
    &lt;span class="k"&gt;for&lt;/span&gt; &lt;span class="n"&gt;i&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;raw&lt;/span&gt; &lt;span class="ow"&gt;in&lt;/span&gt; &lt;span class="nf"&gt;enumerate&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;page&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;ld&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
        &lt;span class="k"&gt;try&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
            &lt;span class="n"&gt;data&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;json&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;loads&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;raw&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
        &lt;span class="k"&gt;except&lt;/span&gt; &lt;span class="n"&gt;json&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;JSONDecodeError&lt;/span&gt; &lt;span class="k"&gt;as&lt;/span&gt; &lt;span class="n"&gt;e&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
            &lt;span class="nf"&gt;print&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sa"&gt;f&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;block &lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="n"&gt;i&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="s"&gt;: INVALID JSON (&lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="n"&gt;e&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="s"&gt;)&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt; &lt;span class="n"&gt;problems&lt;/span&gt; &lt;span class="o"&gt;+=&lt;/span&gt; &lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="k"&gt;continue&lt;/span&gt;
        &lt;span class="k"&gt;for&lt;/span&gt; &lt;span class="n"&gt;path&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;key&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;value&lt;/span&gt; &lt;span class="ow"&gt;in&lt;/span&gt; &lt;span class="nf"&gt;walk&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;data&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
            &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;key&lt;/span&gt; &lt;span class="ow"&gt;in&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;url&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;sameAs&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;}:&lt;/span&gt;
                &lt;span class="n"&gt;ok&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;norm&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;value&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="ow"&gt;in&lt;/span&gt; &lt;span class="n"&gt;page&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;links&lt;/span&gt; &lt;span class="ow"&gt;or&lt;/span&gt; &lt;span class="nf"&gt;norm&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;value&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;==&lt;/span&gt; &lt;span class="nf"&gt;norm&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;url&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
                &lt;span class="n"&gt;where&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;link&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;
            &lt;span class="k"&gt;else&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
                &lt;span class="n"&gt;ok&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;value&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;lower&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="ow"&gt;in&lt;/span&gt; &lt;span class="n"&gt;text&lt;/span&gt;
                &lt;span class="n"&gt;where&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;text&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;
            &lt;span class="n"&gt;soft&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;key&lt;/span&gt; &lt;span class="o"&gt;==&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;sameAs&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;          &lt;span class="c1"&gt;# profile links: review, don't fail
&lt;/span&gt;            &lt;span class="n"&gt;status&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;OK  &lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt; &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;ok&lt;/span&gt; &lt;span class="nf"&gt;else &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;WARN&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt; &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;soft&lt;/span&gt; &lt;span class="k"&gt;else&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;MISS&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
            &lt;span class="n"&gt;problems&lt;/span&gt; &lt;span class="o"&gt;+=&lt;/span&gt; &lt;span class="n"&gt;status&lt;/span&gt; &lt;span class="o"&gt;==&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;MISS&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;
            &lt;span class="nf"&gt;print&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sa"&gt;f&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="n"&gt;status&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="s"&gt; block &lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="n"&gt;i&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="s"&gt; &lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="n"&gt;path&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="s"&gt; = &lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="n"&gt;value&lt;/span&gt;&lt;span class="si"&gt;!r}&lt;/span&gt;&lt;span class="s"&gt; (&lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="n"&gt;where&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="s"&gt;)&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="nf"&gt;print&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sa"&gt;f&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="se"&gt;\n&lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="nf"&gt;len&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;page&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;ld&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="s"&gt; JSON-LD block(s), &lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="n"&gt;problems&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="s"&gt; problem(s)&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="mi"&gt;1&lt;/span&gt; &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;problems&lt;/span&gt; &lt;span class="k"&gt;else&lt;/span&gt; &lt;span class="mi"&gt;0&lt;/span&gt;

&lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;__name__&lt;/span&gt; &lt;span class="o"&gt;==&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;__main__&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
    &lt;span class="n"&gt;sys&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;exit&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nf"&gt;main&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="o"&gt;*&lt;/span&gt;&lt;span class="n"&gt;sys&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;argv&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="mi"&gt;3&lt;/span&gt;&lt;span class="p"&gt;]))&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Run it against a URL, or against a local build with the URL as the base for relative links:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;python3 ldcheck.py https://example.com/
python3 ldcheck.py https://example.com/ dist/index.html
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;For the example page above, the output is:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;OK   block 0 @graph[0].name = 'Example Labs' (text)
OK   block 0 @graph[0].url = 'https://example.com/' (link)
MISS block 0 @graph[0].email = 'support@example.com' (text)
OK   block 0 @graph[0].sameAs[0] = 'https://github.com/example-labs' (link)
WARN block 0 @graph[0].sameAs[1] = 'https://x.com/examplelabs' (link)
OK   block 0 @graph[1].name = 'Example Notes' (text)
OK   block 0 @graph[1].url = 'https://example.com/' (link)
OK   block 0 @graph[1].offers.price = '12' (text)
OK   block 0 @graph[1].offers.priceCurrency = 'USD' (text)

1 JSON-LD block(s), 1 problem(s)
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The fix is on the page side, not the markup side. Show the support email in the footer, add the X link if the account is real and maintained, or remove both from the JSON-LD. The script exits with status 1 when anything is MISS, so you can run it in CI against a preview build and fail the job when someone changes a price in the template but not in the markup.&lt;/p&gt;

&lt;h3&gt;
  
  
  What the script doesn't do
&lt;/h3&gt;

&lt;p&gt;It's a drift detector, not a validator. Know its limits:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Substring matching.&lt;/strong&gt; A price of &lt;code&gt;12&lt;/code&gt; also matches &lt;code&gt;120&lt;/code&gt;. For pricing pages, tighten the check to look for the formatted price string you actually render.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Raw HTML only.&lt;/strong&gt; It reads the HTML the server returns. If your JSON-LD or your visible text is injected by JavaScript, the script won't see it. Google can process JavaScript-generated structured data and &lt;a href="https://developers.google.com/search/docs/appearance/structured-data/generate-structured-data-with-javascript" rel="noopener noreferrer"&gt;documents how&lt;/a&gt;, but not every crawler runs JavaScript, so server-rendering both the facts and the markup is the safer default.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;CSS hiding.&lt;/strong&gt; It skips &lt;code&gt;hidden&lt;/code&gt; and &lt;code&gt;aria-hidden&lt;/code&gt; elements, but not elements hidden with CSS classes. If you hide content in collapsed panels or &lt;code&gt;display: none&lt;/code&gt; rules, check those by hand.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Image URLs.&lt;/strong&gt; &lt;code&gt;logo&lt;/code&gt; and &lt;code&gt;image&lt;/code&gt; paths are skipped on purpose, since a logo URL isn't something a visitor reads as text.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Formatting.&lt;/strong&gt; Phone numbers in E.164 (&lt;code&gt;+1-555-...&lt;/code&gt;) won't match a display format like &lt;code&gt;(555) ...&lt;/code&gt;. Normalize them if you check phones.&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  Validate the syntax and eligibility too
&lt;/h2&gt;

&lt;p&gt;The checker covers the "matches the page" rule. Two official tools cover the rest:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;&lt;a href="https://validator.schema.org/" rel="noopener noreferrer"&gt;Schema.org Validator&lt;/a&gt;&lt;/strong&gt; parses JSON-LD, Microdata and RDFa and shows the extracted items against the schema.org vocabulary. Use it to catch typos in types and property names.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;&lt;a href="https://search.google.com/test/rich-results" rel="noopener noreferrer"&gt;Google Rich Results Test&lt;/a&gt;&lt;/strong&gt; checks whether markup is eligible for Google's rich result types and reports missing required or recommended properties. Google recommends the URL input over the code input, because the code input has JavaScript limitations.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;After deploying, Google's docs suggest using the URL Inspection tool in Search Console to see how Google renders the page, then requesting a recrawl. Even then, Google says it "does not guarantee that features that consume structured data will show up in search results."&lt;/p&gt;

&lt;h2&gt;
  
  
  Why this matters beyond rich results
&lt;/h2&gt;

&lt;p&gt;AI assistants with web search read and quote pages. When the markup and the visible text agree, every reader gets the same facts: a crawler parsing JSON-LD, a model reading page text, and a person checking the source. When they disagree, you've published two versions of your company and can't control which one gets picked up.&lt;/p&gt;

&lt;p&gt;Keep the check cheap and automatic. Run it on the home page, the pricing page and the about page whenever any of them change.&lt;/p&gt;

&lt;p&gt;If you want this kind of check as part of a bigger workflow, &lt;a href="https://aurageo.io/en/" rel="noopener noreferrer"&gt;AURA GEO&lt;/a&gt; can generate JSON-LD from the facts it fetched from your site, and its URL verification stores the raw HTML and JSON-LD a page actually served, so you can compare them. The &lt;a href="https://aurageo.io/en/docs/mcp" rel="noopener noreferrer"&gt;MCP guide&lt;/a&gt; shows how to connect your own agent to run those checks and pick up the resulting tasks.&lt;/p&gt;

</description>
      <category>structureddata</category>
      <category>seo</category>
      <category>python</category>
      <category>webdev</category>
    </item>
    <item>
      <title>Where the AI Money Actually Goes: What Builders Can Learn From 2025–2026 Spending Data</title>
      <dc:creator>AURA GEO</dc:creator>
      <pubDate>Sun, 25 Oct 2026 13:45:48 +0000</pubDate>
      <link>https://dev.to/aurageo/where-the-ai-money-actually-goes-what-builders-can-learn-from-2025-2026-spending-data-2i1i</link>
      <guid>https://dev.to/aurageo/where-the-ai-money-actually-goes-what-builders-can-learn-from-2025-2026-spending-data-2i1i</guid>
      <description>&lt;p&gt;Every week a new AI wrapper launches, and every week someone on a forum asks: &lt;em&gt;"Will anyone actually pay for this?"&lt;/em&gt; Instead of guessing, I went through the public spending reports from Menlo Ventures, Sensor Tower and Appfigures and pulled out the numbers that matter if you build software. This post is a builder's reading of that data: what people pay for, what problem they are buying a fix for, and what that means for the thing you are shipping.&lt;/p&gt;

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

&lt;ul&gt;
&lt;li&gt;Consumer AI spend went from an estimated &lt;strong&gt;$12B (2025) to ~$40B (2026)&lt;/strong&gt;, while US adoption only moved from 61% to 64% (&lt;a href="https://menlovc.com/perspective/2026-the-state-of-consumer-ai/" rel="noopener noreferrer"&gt;Menlo, 2026&lt;/a&gt;). Growth came from existing users paying more, not new users.&lt;/li&gt;
&lt;li&gt;Enterprise gen-AI spend hit &lt;strong&gt;$37B in 2025&lt;/strong&gt;, and &lt;strong&gt;coding alone was $4.0B&lt;/strong&gt;, the largest application category (&lt;a href="https://menlovc.com/perspective/2025-the-state-of-generative-ai-in-the-enterprise/" rel="noopener noreferrer"&gt;Menlo, Enterprise 2025&lt;/a&gt;).&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;27% of enterprise AI app spend arrives through product-led growth&lt;/strong&gt;, vs 7% in traditional software. Individual developers swiping a card is a real go-to-market.&lt;/li&gt;
&lt;li&gt;Users now rank &lt;strong&gt;accuracy (45%) and trustworthiness (40%) above ease of use (32%)&lt;/strong&gt;. That changes what "good UX" means for an AI product.&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  1. People pay to save time on work they already do
&lt;/h2&gt;

&lt;p&gt;The 2026 Menlo consumer survey (5,067 US adults) lists six activities where more than half of the people doing them now use AI:&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Activity&lt;/th&gt;
&lt;th&gt;Share using AI&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Writing support&lt;/td&gt;
&lt;td&gt;61%&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Coding projects&lt;/td&gt;
&lt;td&gt;58%&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;School / work assignments&lt;/td&gt;
&lt;td&gt;56%&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Image creation&lt;/td&gt;
&lt;td&gt;53%&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Presentations&lt;/td&gt;
&lt;td&gt;52%&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Game creation&lt;/td&gt;
&lt;td&gt;52%&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;Look at the list: none of it is a new behaviour. People already wrote, coded, and made slides. AI money flows to &lt;strong&gt;existing, frequent jobs&lt;/strong&gt; where the output is easy to check. If your product idea needs users to adopt a brand-new habit &lt;em&gt;and&lt;/em&gt; trust AI, you are fighting two battles.&lt;/p&gt;

&lt;h2&gt;
  
  
  2. Coding is the first "killer use case", and devs are price-insensitive
&lt;/h2&gt;

&lt;p&gt;On the enterprise side, Menlo estimates departmental AI at $7.3B in 2025, with coding at &lt;strong&gt;$4.0B, 55% of it&lt;/strong&gt;. Next come IT ops ($700M), marketing ($660M) and customer success ($630M).&lt;/p&gt;

&lt;p&gt;The report also notes that "for frontier use cases like coding, users are actually quite price-insensitive and will pay more for performance." That matches what many of us feel personally: if a tool reliably saves an hour a day, $20 vs $40 a month barely registers.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Implication for builders:&lt;/strong&gt; developer tools can price on value and still convert, &lt;em&gt;if&lt;/em&gt; the quality gap is obvious within the first session.&lt;/p&gt;

&lt;h2&gt;
  
  
  3. A small group of power users funds the whole market
&lt;/h2&gt;

&lt;p&gt;This is the most useful number in the 2026 consumer report for anyone designing pricing:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;55% of AI users pay for at least one AI product.&lt;/li&gt;
&lt;li&gt;The typical payer spends &lt;strong&gt;$20–49/month&lt;/strong&gt;.&lt;/li&gt;
&lt;li&gt;But the &lt;strong&gt;14% who spend $100+/month generate 60% of consumer AI spending&lt;/strong&gt;.&lt;/li&gt;
&lt;li&gt;Payers are 5x more likely to use agents (65% vs 13%).&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Pricing models reflect this. Half of payers are on flat monthly plans, 26% annual, 15% mix a subscription with usage charges, and 17% are on usage-based pricing.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Implication:&lt;/strong&gt; a single $9 tier leaves most of the money on the table. A common pattern that fits the data: free tier for discovery → predictable subscription → metered overage for heavy users. Menlo's own advice to founders is blunt: "Keep the free tier." It's where habits form before people pay.&lt;/p&gt;

&lt;h2&gt;
  
  
  4. Thin wrappers make money, but the bulldozer is coming
&lt;/h2&gt;

&lt;p&gt;Appfigures found that third-party "General Assistant" apps (thin wrappers on top of model APIs) earned &lt;strong&gt;$726M in gross revenue since 2023&lt;/strong&gt; among the top 1,000 AI apps, more than ChatGPT's own app category in their dataset at the time (&lt;a href="https://appfigures.com/resources/insights/20250131?f=1" rel="noopener noreferrer"&gt;Appfigures&lt;/a&gt;).&lt;/p&gt;

&lt;p&gt;But Menlo's 2025 consumer report warns that each new model release "absorbs more intelligence and specialized features, making entire categories obsolete overnight," which they compare to "building in front of a moving bulldozer."&lt;/p&gt;

&lt;p&gt;The 2026 data adds nuance: specialized-tool usage rose from 69% to 75% of AI users, and 72% now use both general and specialized products. Users don't replace ChatGPT, they &lt;em&gt;add&lt;/em&gt; tools that do one job materially better. Menlo calls this the difference between being in the "rotation" (competing for queries) and being in the "stack" (owning a workflow).&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Implication:&lt;/strong&gt; a wrapper can earn, but durable revenue comes from owning a workflow: data the general assistant doesn't have, integrations, state, and memory.&lt;/p&gt;

&lt;h2&gt;
  
  
  5. Trust is now a feature you have to ship
&lt;/h2&gt;

&lt;p&gt;In 2025, ease of use ranked ahead of trust. In 2026 the order flipped: accuracy 45%, trustworthiness 40%, security and privacy 36%, ease of use 32%. Among people who only use general assistants, 27% cite privacy concerns with niche tools and 21% say they don't know which tool to trust. Only 11% say they're unaware of a specialized product.&lt;/p&gt;

&lt;p&gt;So the problem isn't discovery. It's credibility. Practical things builders can do:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Show sources and confidence for generated output.&lt;/li&gt;
&lt;li&gt;Ask for the minimum permissions your core job needs, then expand.&lt;/li&gt;
&lt;li&gt;Make output easy to verify (diffs, previews, citations).&lt;/li&gt;
&lt;li&gt;Publish clear data-handling docs.&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  6. Distribution is shifting into AI answers themselves
&lt;/h2&gt;

&lt;p&gt;One more data point that affects how dev tools get discovered. In the 2026 survey, &lt;strong&gt;50% of shoppers who use AI used it to compare prices, 43% to narrow a shortlist, and 37% made a final purchase decision based on its recommendation&lt;/strong&gt;. Menlo also reports that ChatGPT launched ads in February 2026 and, according to OpenAI, passed $1B in run-rate ad revenue about 200 days later.&lt;/p&gt;

&lt;p&gt;For builders, "ask ChatGPT which tool to use" is now a real acquisition channel, next to search and word of mouth. It's worth occasionally asking the main assistants about your own category and seeing whether your product shows up, and what they say about it. We build &lt;a href="https://aurageo.io/en" rel="noopener noreferrer"&gt;AURA GEO&lt;/a&gt; around exactly this kind of check, but you can start manually: ask five assistants the question your user would ask and write down the answers.&lt;/p&gt;

&lt;h2&gt;
  
  
  A builder's checklist
&lt;/h2&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;Pick a job people already do weekly&lt;/strong&gt; and where output is checkable.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Be 10x better at one thing&lt;/strong&gt; than the general assistant your user already has open.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Price for the power-user tail&lt;/strong&gt;: subscription + metered overage, plus a free tier.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Ship trust&lt;/strong&gt;: citations, permissions hygiene, verifiable output.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Plan for PLG&lt;/strong&gt;: individuals adopt first, teams contract later. Menlo notes Cursor reached $200M in revenue before hiring an enterprise sales rep.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Check how AI assistants describe your product&lt;/strong&gt;, because a growing share of buying decisions start there.&lt;/li&gt;
&lt;/ol&gt;

&lt;h2&gt;
  
  
  Closing thought
&lt;/h2&gt;

&lt;p&gt;The data says people aren't paying for "AI". They're paying to get back time on work they already do, and they keep paying when they trust the result. That's a much more boring, and much more buildable, target than most launch posts suggest.&lt;/p&gt;




&lt;h3&gt;
  
  
  Sources
&lt;/h3&gt;

&lt;ul&gt;
&lt;li&gt;Menlo Ventures, &lt;em&gt;2026: The State of Consumer AI&lt;/em&gt; — &lt;a href="https://menlovc.com/perspective/2026-the-state-of-consumer-ai/" rel="noopener noreferrer"&gt;https://menlovc.com/perspective/2026-the-state-of-consumer-ai/&lt;/a&gt;
&lt;/li&gt;
&lt;li&gt;Menlo Ventures, &lt;em&gt;2025: The State of Consumer AI&lt;/em&gt; — &lt;a href="https://menlovc.com/perspective/2025-the-state-of-consumer-ai/" rel="noopener noreferrer"&gt;https://menlovc.com/perspective/2025-the-state-of-consumer-ai/&lt;/a&gt;
&lt;/li&gt;
&lt;li&gt;Menlo Ventures, &lt;em&gt;2025: The State of Generative AI in the Enterprise&lt;/em&gt; — &lt;a href="https://menlovc.com/perspective/2025-the-state-of-generative-ai-in-the-enterprise/" rel="noopener noreferrer"&gt;https://menlovc.com/perspective/2025-the-state-of-generative-ai-in-the-enterprise/&lt;/a&gt;
&lt;/li&gt;
&lt;li&gt;Appfigures, &lt;em&gt;AI Apps are a $2B Opportunity and Thin Wrappers Make More Than You Think&lt;/em&gt; — &lt;a href="https://appfigures.com/resources/insights/20250131?f=1" rel="noopener noreferrer"&gt;https://appfigures.com/resources/insights/20250131?f=1&lt;/a&gt;
&lt;/li&gt;
&lt;/ul&gt;

</description>
      <category>ai</category>
      <category>saas</category>
      <category>startup</category>
      <category>marketing</category>
    </item>
    <item>
      <title>Allow AI Search, Opt Out of Training: A Granular robots.txt You Can Test in Python</title>
      <dc:creator>AURA GEO</dc:creator>
      <pubDate>Sat, 24 Oct 2026 16:15:11 +0000</pubDate>
      <link>https://dev.to/aurageo/allow-ai-search-opt-out-of-training-a-granular-robotstxt-you-can-test-in-python-4nih</link>
      <guid>https://dev.to/aurageo/allow-ai-search-opt-out-of-training-a-granular-robotstxt-you-can-test-in-python-4nih</guid>
      <description>&lt;p&gt;Most robots.txt advice about AI comes in two flavors: "block all AI bots" or "allow everything." Both skip the useful middle option. You can let AI search and user-requested fetches reach your pages while telling the training crawlers to stay out.&lt;/p&gt;

&lt;p&gt;That only works if you know which token does which job, and if your file actually matches the way crawlers read groups. This post covers both, then tests the result with about 30 lines of Python and a couple of &lt;code&gt;curl&lt;/code&gt; calls.&lt;/p&gt;

&lt;p&gt;Everything below comes from the vendors' own documentation as of October 2026. Tokens and policies change, so check the linked pages before you deploy.&lt;/p&gt;

&lt;h2&gt;
  
  
  Three jobs, three kinds of bots
&lt;/h2&gt;

&lt;p&gt;Each large AI operator separates crawling by purpose. Blocking one token doesn't block the others.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Search and indexing&lt;/strong&gt; (they decide whether you can be shown and linked in answers):&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;code&gt;OAI-SearchBot&lt;/code&gt;: OpenAI says it is used "to surface websites in search results in ChatGPT's search features," and sites opted out of it "will not be shown in ChatGPT search answers" (&lt;a href="https://platform.openai.com/docs/bots" rel="noopener noreferrer"&gt;OpenAI crawlers&lt;/a&gt;).&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;Claude-SearchBot&lt;/code&gt;: per &lt;a href="https://support.claude.com/en/articles/8896518-does-anthropic-crawl-data-from-the-web-and-how-can-site-owners-block-the-crawler" rel="noopener noreferrer"&gt;Anthropic&lt;/a&gt;, blocking it "may reduce your site's visibility and accuracy in user search results."&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;PerplexityBot&lt;/code&gt;: "designed to surface and link websites in search results on Perplexity," and not used to crawl content for AI foundation models (&lt;a href="https://docs.perplexity.ai/guides/bots" rel="noopener noreferrer"&gt;Perplexity crawlers&lt;/a&gt;).&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;Bingbot&lt;/code&gt;: Bing's standard crawler (&lt;a href="https://www.bing.com/webmasters/help/which-crawlers-does-bing-use-8c184ec0" rel="noopener noreferrer"&gt;Bing crawlers&lt;/a&gt;).&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;Googlebot&lt;/code&gt;: Google says robots.txt rules for Googlebot are the control for Search, including AI Overviews and AI Mode (&lt;a href="https://developers.google.com/search/docs/appearance/ai-features" rel="noopener noreferrer"&gt;AI features and your website&lt;/a&gt;).&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;Applebot&lt;/code&gt;: powers search in Spotlight, Siri and Safari (&lt;a href="https://support.apple.com/en-us/119829" rel="noopener noreferrer"&gt;About Applebot&lt;/a&gt;).&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;strong&gt;User-triggered fetchers&lt;/strong&gt; (they load a page because a person asked a question just now):&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;code&gt;ChatGPT-User&lt;/code&gt;: OpenAI says that "because these actions are initiated by a user, robots.txt rules may not apply."&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;Perplexity-User&lt;/code&gt;: Perplexity says that "since a user requested the fetch, this fetcher generally ignores robots.txt rules."&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;Claude-User&lt;/code&gt;: Anthropic describes it as a way for site owners to control user-initiated access, and says its bots honor robots.txt.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;strong&gt;Training and dataset controls:&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;code&gt;GPTBot&lt;/code&gt;: disallowing it "indicates a site's content should not be used in training" OpenAI's foundation models.&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;ClaudeBot&lt;/code&gt;: restricting it signals that future content "should be excluded from our AI model training datasets."&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;Google-Extended&lt;/code&gt;: not a crawler. It's a control token with no user agent of its own. It governs use of Google-crawled content for training future Gemini models and for grounding in Gemini Apps and Vertex AI, and Google says it "does not impact a site's inclusion in Google Search" (&lt;a href="https://developers.google.com/crawling/docs/crawlers-fetchers/google-common-crawlers" rel="noopener noreferrer"&gt;Google's common crawlers&lt;/a&gt;).&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;Applebot-Extended&lt;/code&gt;: also doesn't crawl. Disallowing it opts you out of training Apple's foundation models, and those pages "can still be included in search results."&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;CCBot&lt;/code&gt;: the crawler of &lt;a href="https://commoncrawl.org/ccbot" rel="noopener noreferrer"&gt;Common Crawl&lt;/a&gt;, a non-profit that publishes an open crawl archive anyone can use. Blocking it keeps future pages out of that archive.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Two operators don't fit this pattern. &lt;strong&gt;Microsoft&lt;/strong&gt; has no separate training token in robots.txt. Its controls for Copilot and training are the &lt;code&gt;noarchive&lt;/code&gt; and &lt;code&gt;nocache&lt;/code&gt; robots meta values (&lt;a href="https://www.bing.com/webmasters/help/robots-meta-tags-and-attributes-that-bing-supports-5198d240" rel="noopener noreferrer"&gt;Bing robots meta tags&lt;/a&gt;). &lt;strong&gt;Apple&lt;/strong&gt; has a fallback rule: "If robots instructions don't mention Applebot but mention Googlebot, the Apple robot will follow Googlebot instructions." A Googlebot-only group therefore quietly governs Applebot too.&lt;/p&gt;

&lt;h2&gt;
  
  
  How crawlers pick a group (RFC 9309)
&lt;/h2&gt;

&lt;p&gt;&lt;a href="https://www.rfc-editor.org/rfc/rfc9309" rel="noopener noreferrer"&gt;RFC 9309&lt;/a&gt; is the robots.txt standard. Four rules from it cause most of the surprises:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;A crawler obeys the group that names its product token.&lt;/strong&gt; Matching is case-insensitive. The &lt;code&gt;*&lt;/code&gt; group applies only "if no matching group exists."&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;A named group doesn't inherit anything from &lt;code&gt;*&lt;/code&gt;.&lt;/strong&gt; If &lt;code&gt;OAI-SearchBot&lt;/code&gt; has its own group, every line under &lt;code&gt;User-agent: *&lt;/code&gt; is irrelevant to it.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Groups naming the same token are merged.&lt;/strong&gt; Two &lt;code&gt;User-agent: GPTBot&lt;/code&gt; blocks combine into one rule set (the RFC has an example). Many homemade parsers get this wrong, so don't depend on it. Keep one group per token.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Within a group, the most specific path wins&lt;/strong&gt;, meaning the longest match. If an &lt;code&gt;Allow&lt;/code&gt; and a &lt;code&gt;Disallow&lt;/code&gt; are equivalent, &lt;code&gt;Allow&lt;/code&gt; should win.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;Here's the bug rule 2 produces, and it's common:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;User-agent: *
Disallow: /admin/

User-agent: OAI-SearchBot
Allow: /
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The author wanted "everyone stays out of /admin/, and OpenAI search is explicitly welcome." What they got: OAI-SearchBot obeys only its own group, so &lt;code&gt;/admin/&lt;/code&gt; is allowed for it. &lt;strong&gt;Every named group has to repeat the private paths.&lt;/strong&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  A granular file
&lt;/h2&gt;

&lt;p&gt;Here is a file that allows search and user fetches, keeps private paths private for everyone, and opts out of training:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;# 1) Training and dataset opt-outs
User-agent: GPTBot
User-agent: ClaudeBot
User-agent: Google-Extended
User-agent: Applebot-Extended
User-agent: CCBot
Disallow: /

# 2) Search and user-triggered fetchers: allowed, same private paths
User-agent: OAI-SearchBot
User-agent: ChatGPT-User
User-agent: Claude-SearchBot
User-agent: Claude-User
User-agent: PerplexityBot
User-agent: Perplexity-User
User-agent: Bingbot
User-agent: Googlebot
User-agent: Applebot
Disallow: /admin/
Disallow: /cart/

# 3) Everyone else
User-agent: *
Disallow: /admin/
Disallow: /cart/

Sitemap: https://example.com/sitemap.xml
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;A few notes:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Several &lt;code&gt;User-agent&lt;/code&gt; lines in a row form one group. That's valid under the RFC and saves repetition.&lt;/li&gt;
&lt;li&gt;The training group comes first on purpose. That's not because of the RFC, where order doesn't matter, but because of a Python quirk covered below.&lt;/li&gt;
&lt;li&gt;Opting out of training is a business decision, not a GEO requirement. If you're happy to be used for training, delete group 1 and the &lt;code&gt;*&lt;/code&gt; group covers those bots.&lt;/li&gt;
&lt;li&gt;Remember what robots.txt can't do. &lt;code&gt;ChatGPT-User&lt;/code&gt; and &lt;code&gt;Perplexity-User&lt;/code&gt; say they may not follow it. To actually stop a client, you need server or WAF rules.&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  Test it with Python
&lt;/h2&gt;

&lt;p&gt;Python's standard library includes &lt;code&gt;urllib.robotparser&lt;/code&gt;. This script prints an allow/block matrix for every token against a few paths. It works on a local file or a live URL:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;sys&lt;/span&gt;
&lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;urllib.robotparser&lt;/span&gt;

&lt;span class="c1"&gt;# python3 check_robots.py robots.txt
# python3 check_robots.py https://example.com/robots.txt
&lt;/span&gt;&lt;span class="n"&gt;src&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;sys&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;argv&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;
&lt;span class="n"&gt;rp&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;urllib&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;robotparser&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nc"&gt;RobotFileParser&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
&lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;src&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;startswith&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;http&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
    &lt;span class="n"&gt;rp&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;set_url&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;src&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="n"&gt;rp&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;read&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
&lt;span class="k"&gt;else&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
    &lt;span class="k"&gt;with&lt;/span&gt; &lt;span class="nf"&gt;open&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;src&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;encoding&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;utf-8&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;as&lt;/span&gt; &lt;span class="n"&gt;f&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
        &lt;span class="n"&gt;rp&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;parse&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;f&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;read&lt;/span&gt;&lt;span class="p"&gt;().&lt;/span&gt;&lt;span class="nf"&gt;splitlines&lt;/span&gt;&lt;span class="p"&gt;())&lt;/span&gt;

&lt;span class="n"&gt;AGENTS&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;
    &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;OAI-SearchBot&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;ChatGPT-User&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;GPTBot&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;Claude-SearchBot&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;Claude-User&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;ClaudeBot&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;PerplexityBot&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;Perplexity-User&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;Bingbot&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;Googlebot&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;Google-Extended&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;Applebot&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;Applebot-Extended&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;CCBot&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;SomeOtherBot&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
&lt;span class="p"&gt;]&lt;/span&gt;
&lt;span class="n"&gt;PATHS&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;/&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;/pricing&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;/blog/post&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;/admin/&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;/cart/checkout&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;

&lt;span class="n"&gt;width&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;max&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nf"&gt;len&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;a&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;for&lt;/span&gt; &lt;span class="n"&gt;a&lt;/span&gt; &lt;span class="ow"&gt;in&lt;/span&gt; &lt;span class="n"&gt;AGENTS&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="nf"&gt;print&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt; &lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt; &lt;span class="n"&gt;width&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="n"&gt;p&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;ljust&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;14&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;for&lt;/span&gt; &lt;span class="n"&gt;p&lt;/span&gt; &lt;span class="ow"&gt;in&lt;/span&gt; &lt;span class="n"&gt;PATHS&lt;/span&gt;&lt;span class="p"&gt;])&lt;/span&gt;
&lt;span class="k"&gt;for&lt;/span&gt; &lt;span class="n"&gt;agent&lt;/span&gt; &lt;span class="ow"&gt;in&lt;/span&gt; &lt;span class="n"&gt;AGENTS&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
    &lt;span class="n"&gt;row&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;allow&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt; &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;rp&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;can_fetch&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;agent&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;p&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;else&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;BLOCK&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt; &lt;span class="k"&gt;for&lt;/span&gt; &lt;span class="n"&gt;p&lt;/span&gt; &lt;span class="ow"&gt;in&lt;/span&gt; &lt;span class="n"&gt;PATHS&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;
    &lt;span class="nf"&gt;print&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;agent&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;ljust&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;width&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="n"&gt;r&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;ljust&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;14&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;for&lt;/span&gt; &lt;span class="n"&gt;r&lt;/span&gt; &lt;span class="ow"&gt;in&lt;/span&gt; &lt;span class="n"&gt;row&lt;/span&gt;&lt;span class="p"&gt;])&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Running it against the file above (Python 3.13) prints:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;                  /              /pricing       /blog/post     /admin/        /cart/checkout
OAI-SearchBot     allow          allow          allow          BLOCK          BLOCK
ChatGPT-User      allow          allow          allow          BLOCK          BLOCK
GPTBot            BLOCK          BLOCK          BLOCK          BLOCK          BLOCK
Claude-SearchBot  allow          allow          allow          BLOCK          BLOCK
Claude-User       allow          allow          allow          BLOCK          BLOCK
ClaudeBot         BLOCK          BLOCK          BLOCK          BLOCK          BLOCK
PerplexityBot     allow          allow          allow          BLOCK          BLOCK
Perplexity-User   allow          allow          allow          BLOCK          BLOCK
Bingbot           allow          allow          allow          BLOCK          BLOCK
Googlebot         allow          allow          allow          BLOCK          BLOCK
Google-Extended   BLOCK          BLOCK          BLOCK          BLOCK          BLOCK
Applebot          allow          allow          allow          BLOCK          BLOCK
Applebot-Extended BLOCK          BLOCK          BLOCK          BLOCK          BLOCK
CCBot             BLOCK          BLOCK          BLOCK          BLOCK          BLOCK
SomeOtherBot      allow          allow          allow          BLOCK          BLOCK
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Run it against the buggy two-group example and you'll see &lt;code&gt;OAI-SearchBot&lt;/code&gt; allowed into &lt;code&gt;/admin/&lt;/code&gt;. That's the inheritance bug made visible.&lt;/p&gt;

&lt;h3&gt;
  
  
  Know the parser's limits
&lt;/h3&gt;

&lt;p&gt;&lt;code&gt;urllib.robotparser&lt;/code&gt; is handy, but it's not an RFC 9309 implementation. Reading &lt;a href="https://github.com/python/cpython/blob/main/Lib/urllib/robotparser.py" rel="noopener noreferrer"&gt;its source&lt;/a&gt; shows three differences:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;It uses the first matching group and matches substrings.&lt;/strong&gt; It checks whether a group's token appears inside the name you pass in. When I first put the search group (which contains &lt;code&gt;Applebot&lt;/code&gt;) above the training group, &lt;code&gt;Applebot-Extended&lt;/code&gt; matched &lt;code&gt;Applebot&lt;/code&gt; and came out as "allow." Putting the training group first fixed the test. Real crawlers match product tokens, but the lesson stands: test exactly the tokens you care about.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;It uses the first matching rule, not the longest one.&lt;/strong&gt; Overlapping &lt;code&gt;Allow&lt;/code&gt;/&lt;code&gt;Disallow&lt;/code&gt; pairs can give a different answer than the RFC.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;It doesn't support &lt;code&gt;*&lt;/code&gt; and &lt;code&gt;$&lt;/code&gt; path wildcards.&lt;/strong&gt; Google documents those and many sites use them.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;So use it as a fast smoke test for files with one group per token and no overlapping or wildcard rules. For anything more complex, also test with a parser that implements the standard, such as &lt;a href="https://github.com/google/robotstxt" rel="noopener noreferrer"&gt;Google's open-source robots.txt library&lt;/a&gt;.&lt;/p&gt;

&lt;h2&gt;
  
  
  Test the live file with curl
&lt;/h2&gt;

&lt;p&gt;The parser tells you what the file means. &lt;code&gt;curl&lt;/code&gt; tells you what crawlers actually receive:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="c"&gt;# 1. Status, content type, redirects&lt;/span&gt;
curl &lt;span class="nt"&gt;-sSI&lt;/span&gt; https://example.com/robots.txt

&lt;span class="c"&gt;# 2. The exact body, as a crawler gets it&lt;/span&gt;
curl &lt;span class="nt"&gt;-s&lt;/span&gt; &lt;span class="nt"&gt;-A&lt;/span&gt; &lt;span class="s2"&gt;"Mozilla/5.0 AppleWebKit/537.36 (KHTML, like Gecko); compatible; GPTBot/1.4; +https://openai.com/gptbot"&lt;/span&gt; &lt;span class="se"&gt;\&lt;/span&gt;
  https://example.com/robots.txt

&lt;span class="c"&gt;# 3. Compare with a browser user agent&lt;/span&gt;
diff &amp;lt;&lt;span class="o"&gt;(&lt;/span&gt;curl &lt;span class="nt"&gt;-s&lt;/span&gt; https://example.com/robots.txt&lt;span class="o"&gt;)&lt;/span&gt; &lt;span class="se"&gt;\&lt;/span&gt;
     &amp;lt;&lt;span class="o"&gt;(&lt;/span&gt;curl &lt;span class="nt"&gt;-s&lt;/span&gt; &lt;span class="nt"&gt;-A&lt;/span&gt; &lt;span class="s2"&gt;"Mozilla/5.0 AppleWebKit/537.36 (KHTML, like Gecko; compatible; PerplexityBot/1.0; +https://perplexity.ai/perplexitybot)"&lt;/span&gt; https://example.com/robots.txt&lt;span class="o"&gt;)&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;What to look for, according to RFC 9309:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;A 5xx response is the worst case.&lt;/strong&gt; The file is "unreachable," and crawlers must assume complete disallow. A flaky origin or an edge rule that errors on bots can silently block everything.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;A 4xx response means "unavailable,"&lt;/strong&gt; and crawlers may access anything. Your carefully written opt-outs simply don't exist.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Redirects:&lt;/strong&gt; crawlers should follow at least five. A redirect to an HTML page (a login, a 404 template) gives them something that isn't a robots file.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Size:&lt;/strong&gt; parsers must handle at least 500 KiB. Keep the file far below that.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Different body per user agent:&lt;/strong&gt; a CDN or bot-protection layer may be rewriting or challenging the request.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Finally, allow for delay. The RFC says crawlers shouldn't use a cached copy for more than 24 hours. OpenAI says search changes can take "~24 hours." Perplexity says "up to 24 hours." Check again the next day.&lt;/p&gt;

&lt;h2&gt;
  
  
  Checklist
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;One group per token. No duplicated &lt;code&gt;User-agent: X&lt;/code&gt; blocks.&lt;/li&gt;
&lt;li&gt;Private paths repeated in every named group.&lt;/li&gt;
&lt;li&gt;A deliberate decision on each training token. Don't rely on copied defaults.&lt;/li&gt;
&lt;li&gt;Googlebot rules reviewed with Applebot in mind (Apple's fallback).&lt;/li&gt;
&lt;li&gt;The live file returns &lt;code&gt;200&lt;/code&gt;, &lt;code&gt;text/plain&lt;/code&gt;, and the same body for bots and browsers.&lt;/li&gt;
&lt;li&gt;The matrix script run against the live URL after every deploy.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;If you manage AI visibility with an agent, &lt;a href="https://aurageo.io/en/" rel="noopener noreferrer"&gt;AURA GEO&lt;/a&gt; can generate a robots.txt patch from your site's real state. The tool description is explicit that it must be merged into your existing policy, never blindly swapped in. Run the script above on the merged result before you ship it. For background on the tokens themselves, see AURA's knowledge-base note on &lt;a href="https://aurageo.io/en/learn/robots-txt-for-ai-search/" rel="noopener noreferrer"&gt;robots.txt for AI search&lt;/a&gt;.&lt;/p&gt;

</description>
      <category>robotstxt</category>
      <category>seo</category>
      <category>ai</category>
      <category>python</category>
    </item>
    <item>
      <title>Moved to Cloudflare? Check It Isn't Quietly Blocking AI Crawlers</title>
      <dc:creator>AURA GEO</dc:creator>
      <pubDate>Fri, 23 Oct 2026 16:19:02 +0000</pubDate>
      <link>https://dev.to/aurageo/moved-to-cloudflare-check-it-isnt-quietly-blocking-ai-crawlers-lol</link>
      <guid>https://dev.to/aurageo/moved-to-cloudflare-check-it-isnt-quietly-blocking-ai-crawlers-lol</guid>
      <description>&lt;p&gt;You moved your site onto Cloudflare. DNS resolved, the padlock is green, every page loads in your browser. Job done?&lt;/p&gt;

&lt;p&gt;Not quite. Cloudflare now decides, at the edge, what each client gets, and several of its features treat AI crawlers differently from people. If you never opened those settings, you don't actually know whether ChatGPT search, Claude or Perplexity can still reach your pages.&lt;/p&gt;

&lt;p&gt;This post walks through the settings that matter (as Cloudflare documents them in October 2026), a &lt;code&gt;curl&lt;/code&gt; test you can run in a few minutes, and how to read what comes back.&lt;/p&gt;

&lt;h2&gt;
  
  
  First, know which bot does what
&lt;/h2&gt;

&lt;p&gt;"AI bots" is not one thing. The big operators run separate user agents for training, for search, and for fetching a page because a person asked a question. Blocking one does not block the others.&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Operator&lt;/th&gt;
&lt;th&gt;Training&lt;/th&gt;
&lt;th&gt;Search / indexing&lt;/th&gt;
&lt;th&gt;User-triggered fetch&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;OpenAI&lt;/td&gt;
&lt;td&gt;&lt;code&gt;GPTBot&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;&lt;code&gt;OAI-SearchBot&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;&lt;code&gt;ChatGPT-User&lt;/code&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Anthropic&lt;/td&gt;
&lt;td&gt;&lt;code&gt;ClaudeBot&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;&lt;code&gt;Claude-SearchBot&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;&lt;code&gt;Claude-User&lt;/code&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Perplexity&lt;/td&gt;
&lt;td&gt;(none listed)&lt;/td&gt;
&lt;td&gt;&lt;code&gt;PerplexityBot&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;&lt;code&gt;Perplexity-User&lt;/code&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;Sources: &lt;a href="https://platform.openai.com/docs/bots" rel="noopener noreferrer"&gt;OpenAI crawlers&lt;/a&gt;, &lt;a href="https://support.claude.com/en/articles/8896518-does-anthropic-crawl-data-from-the-web-and-how-can-site-owners-block-the-crawler" rel="noopener noreferrer"&gt;Anthropic's crawler FAQ&lt;/a&gt;, &lt;a href="https://docs.perplexity.ai/guides/bots" rel="noopener noreferrer"&gt;Perplexity crawlers&lt;/a&gt;.&lt;/p&gt;

&lt;p&gt;Two details from those pages matter here. OpenAI says sites opted out of &lt;code&gt;OAI-SearchBot&lt;/code&gt; won't be shown in ChatGPT search answers. And both OpenAI and Perplexity say their user-triggered fetchers (&lt;code&gt;ChatGPT-User&lt;/code&gt;, &lt;code&gt;Perplexity-User&lt;/code&gt;) may not follow robots.txt, because a person asked for the page. So "should I let AI bots in?" is really three questions: training, search, and live fetches.&lt;/p&gt;

&lt;h2&gt;
  
  
  The Cloudflare settings that can affect AI crawlers
&lt;/h2&gt;

&lt;p&gt;There are five places to look. They overlap, so check all of them.&lt;/p&gt;

&lt;h2&gt;
  
  
  1. AI bot policies: Search, Agent, Training
&lt;/h2&gt;

&lt;p&gt;Cloudflare now groups AI traffic by behavior (&lt;a href="https://developers.cloudflare.com/bots/concepts/bot/" rel="noopener noreferrer"&gt;Bots: AI bots&lt;/a&gt;):&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Search&lt;/strong&gt;: collects or indexes your content so it can answer questions later.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Agent&lt;/strong&gt;: acts in real time on a person's behalf, "such as chat fetch bots and browser-use agents."&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Training&lt;/strong&gt;: crawls content to train or fine-tune a model.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;You'll find them under &lt;strong&gt;Security Settings &amp;gt; Configure AI bot policies&lt;/strong&gt;. For each one you can choose Block, Block on pages with ads, or Allow (&lt;a href="https://developers.cloudflare.com/bots/additional-configurations/block-ai-bots/" rel="noopener noreferrer"&gt;Block AI Bots docs&lt;/a&gt;). Since September 15, 2026, Training also has a &lt;strong&gt;Disallow AI Training&lt;/strong&gt; option. According to &lt;a href="https://blog.cloudflare.com/accountable-mixed-use-ai-crawlers/" rel="noopener noreferrer"&gt;Cloudflare's announcement&lt;/a&gt;, it publishes a no-training preference in robots.txt, keeps crawlers Cloudflare calls "Accountable" mixed-use crawlers (Applebot, Bingbot, Googlebot) allowed for search, and blocks other training crawlers.&lt;/p&gt;

&lt;p&gt;Two things in that announcement deserve attention:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Block now reaches search engines too.&lt;/strong&gt; Block and Block on pages with ads for Training now also apply to mixed-use crawlers, "including Applebot, Bingbot, and Googlebot, so either setting impacts search as well as training."&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;The old switch is gone.&lt;/strong&gt; The legacy "Block AI bots" toggle is deprecated, and existing settings were migrated to the new controls. If you had the old toggle on, check what it turned into.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;For new domains, Cloudflare proposes presets during onboarding. Per the same post, a site that doesn't monetize with ads gets Search, Training and Agent all set to Allow. A site that monetizes with ads gets Search Allow, Training Disallow AI Training and Agent Block on pages with ads. You can change any of them later. Don't assume. Open the page and read what's actually set.&lt;/p&gt;

&lt;h2&gt;
  
  
  2. Bot Preference Sync (formerly managed robots.txt)
&lt;/h2&gt;

&lt;p&gt;&lt;a href="https://developers.cloudflare.com/bots/additional-configurations/managed-robots-txt/" rel="noopener noreferrer"&gt;Bot Preference Sync&lt;/a&gt; writes your policy choices into robots.txt. When it's on and at least one policy is set to Block, Block on pages with ads or (for Training) Disallow, Cloudflare &lt;strong&gt;prepends&lt;/strong&gt; managed directives and its Content Signals Policy to your existing file. If you have no robots.txt, Cloudflare creates one. &lt;a href="https://blog.cloudflare.com/bot-preference-sync/" rel="noopener noreferrer"&gt;Cloudflare's blog&lt;/a&gt; says it's on by default for new customers.&lt;/p&gt;

&lt;p&gt;Content Signals are three machine-readable categories: &lt;code&gt;search&lt;/code&gt;, &lt;code&gt;ai-input&lt;/code&gt; and &lt;code&gt;ai-train&lt;/code&gt;. Cloudflare's docs are clear that robots.txt compliance is voluntary. It states a preference, and enforcement happens elsewhere.&lt;/p&gt;

&lt;p&gt;So your live robots.txt may no longer be the file you deployed.&lt;/p&gt;

&lt;h2&gt;
  
  
  3. AI Crawl Control
&lt;/h2&gt;

&lt;p&gt;&lt;a href="https://developers.cloudflare.com/ai-crawl-control/" rel="noopener noreferrer"&gt;AI Crawl Control&lt;/a&gt; (formerly AI Audit, available on all plans) shows which AI crawlers request your content, and lets you Allow or Block each one (&lt;a href="https://developers.cloudflare.com/ai-crawl-control/features/manage-ai-crawlers/" rel="noopener noreferrer"&gt;Manage AI crawlers&lt;/a&gt;). Blocking a crawler there creates or updates a WAF custom rule. On paid plans you can choose whether blocked crawlers get &lt;code&gt;403 Forbidden&lt;/code&gt; or &lt;code&gt;402 Payment Required&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;One detail matters for testing. On the Free plan, AI Crawl Control identifies AI crawlers "based on their user agent strings." Paid plans can use Bot Management detection instead.&lt;/p&gt;

&lt;h2&gt;
  
  
  4. Bot Fight Mode
&lt;/h2&gt;

&lt;p&gt;&lt;a href="https://developers.cloudflare.com/bots/get-started/bot-fight-mode/" rel="noopener noreferrer"&gt;Bot Fight Mode&lt;/a&gt; is a free toggle that identifies traffic matching known bot patterns and issues computationally expensive challenges. Cloudflare notes it "may challenge API or mobile app traffic". Also, &lt;strong&gt;you can't bypass it with WAF custom rules or Page Rules&lt;/strong&gt;: Skip, Bypass and Allow have no effect on it. If it gets in your way, the documented options are turning it off or moving to Super Bot Fight Mode, which supports Skip rules.&lt;/p&gt;

&lt;h2&gt;
  
  
  5. Your own WAF custom rules
&lt;/h2&gt;

&lt;p&gt;Country blocks or a user-agent blocklist copied from a forum can catch crawlers too. The &lt;a href="https://developers.cloudflare.com/ruleset-engine/rules-language/actions/" rel="noopener noreferrer"&gt;Block action&lt;/a&gt; returns &lt;code&gt;403&lt;/code&gt; for most security features (or &lt;code&gt;429&lt;/code&gt; for rate limiting). Challenge actions require the client to pass a Cloudflare challenge first. Anthropic, for example, says its bots won't try to bypass CAPTCHAs.&lt;/p&gt;

&lt;p&gt;If you write allow rules, Cloudflare exposes &lt;code&gt;cf.client.bot&lt;/code&gt; (a request from a known good bot) and &lt;code&gt;cf.verified_bot_category&lt;/code&gt;. That's safer than trusting the User-Agent header, which anyone can fake. Perplexity's own docs recommend combining user-agent matching with its published IP ranges.&lt;/p&gt;

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

&lt;p&gt;Send the same request with different user agents and compare. The strings below come from the vendors' docs; version numbers may change. Anthropic doesn't publish a full UA string on that page, so the ClaudeBot line is a minimal test string containing the product token.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="nv"&gt;URL&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="s2"&gt;"https://example.com/pricing"&lt;/span&gt;

&lt;span class="nb"&gt;declare&lt;/span&gt; &lt;span class="nt"&gt;-A&lt;/span&gt; &lt;span class="nv"&gt;UA&lt;/span&gt;&lt;span class="o"&gt;=(&lt;/span&gt;
  &lt;span class="o"&gt;[&lt;/span&gt;browser]&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="s2"&gt;"Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/131.0.0.0 Safari/537.36"&lt;/span&gt;
  &lt;span class="o"&gt;[&lt;/span&gt;GPTBot]&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="s2"&gt;"Mozilla/5.0 AppleWebKit/537.36 (KHTML, like Gecko); compatible; GPTBot/1.4; +https://openai.com/gptbot"&lt;/span&gt;
  &lt;span class="o"&gt;[&lt;/span&gt;OAI-SearchBot]&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="s2"&gt;"Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/131.0.0.0 Safari/537.36; compatible; OAI-SearchBot/1.4; +https://openai.com/searchbot"&lt;/span&gt;
  &lt;span class="o"&gt;[&lt;/span&gt;ChatGPT-User]&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="s2"&gt;"Mozilla/5.0 AppleWebKit/537.36 (KHTML, like Gecko); compatible; ChatGPT-User/1.0; +https://openai.com/bot"&lt;/span&gt;
  &lt;span class="o"&gt;[&lt;/span&gt;PerplexityBot]&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="s2"&gt;"Mozilla/5.0 AppleWebKit/537.36 (KHTML, like Gecko; compatible; PerplexityBot/1.0; +https://perplexity.ai/perplexitybot)"&lt;/span&gt;
  &lt;span class="o"&gt;[&lt;/span&gt;Perplexity-User]&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="s2"&gt;"Mozilla/5.0 AppleWebKit/537.36 (KHTML, like Gecko; compatible; Perplexity-User/1.0; +https://perplexity.ai/perplexity-user)"&lt;/span&gt;
  &lt;span class="o"&gt;[&lt;/span&gt;ClaudeBot]&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="s2"&gt;"Mozilla/5.0 (compatible; ClaudeBot/1.0)"&lt;/span&gt;
&lt;span class="o"&gt;)&lt;/span&gt;

&lt;span class="k"&gt;for &lt;/span&gt;name &lt;span class="k"&gt;in&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="k"&gt;${&lt;/span&gt;&lt;span class="p"&gt;!UA[@]&lt;/span&gt;&lt;span class="k"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="k"&gt;do
  &lt;/span&gt;curl &lt;span class="nt"&gt;-s&lt;/span&gt; &lt;span class="nt"&gt;-o&lt;/span&gt; &lt;span class="s2"&gt;"out_&lt;/span&gt;&lt;span class="nv"&gt;$name&lt;/span&gt;&lt;span class="s2"&gt;.html"&lt;/span&gt; &lt;span class="nt"&gt;-D&lt;/span&gt; &lt;span class="s2"&gt;"hdr_&lt;/span&gt;&lt;span class="nv"&gt;$name&lt;/span&gt;&lt;span class="s2"&gt;.txt"&lt;/span&gt; &lt;span class="nt"&gt;-A&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="k"&gt;${&lt;/span&gt;&lt;span class="nv"&gt;UA&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="nv"&gt;$name&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;&lt;span class="k"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt; &lt;span class="se"&gt;\&lt;/span&gt;
    &lt;span class="nt"&gt;-w&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="nv"&gt;$name&lt;/span&gt;&lt;span class="se"&gt;\t&lt;/span&gt;&lt;span class="s2"&gt;%{http_code}&lt;/span&gt;&lt;span class="se"&gt;\t&lt;/span&gt;&lt;span class="s2"&gt;%{size_download} bytes&lt;/span&gt;&lt;span class="se"&gt;\n&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="nv"&gt;$URL&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt;
  &lt;span class="nb"&gt;grep&lt;/span&gt; &lt;span class="nt"&gt;-i&lt;/span&gt; &lt;span class="nt"&gt;-E&lt;/span&gt; &lt;span class="s2"&gt;"^(cf-mitigated|cf-ray):"&lt;/span&gt; &lt;span class="s2"&gt;"hdr_&lt;/span&gt;&lt;span class="nv"&gt;$name&lt;/span&gt;&lt;span class="s2"&gt;.txt"&lt;/span&gt; | &lt;span class="nb"&gt;sed&lt;/span&gt; &lt;span class="s1"&gt;'s/^/    /'&lt;/span&gt;
&lt;span class="k"&gt;done&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Then check what robots.txt says now:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;curl &lt;span class="nt"&gt;-s&lt;/span&gt; https://example.com/robots.txt | &lt;span class="nb"&gt;sed&lt;/span&gt; &lt;span class="nt"&gt;-n&lt;/span&gt; &lt;span class="s1"&gt;'1,80p'&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Look for a &lt;code&gt;BEGIN Cloudflare&lt;/code&gt; block, &lt;code&gt;Content-signal:&lt;/code&gt; lines, and &lt;code&gt;Disallow: /&lt;/code&gt; under any user agent you care about.&lt;/p&gt;

&lt;h2&gt;
  
  
  Reading the result
&lt;/h2&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;What you see&lt;/th&gt;
&lt;th&gt;What it means&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Same status and similar size as the browser&lt;/td&gt;
&lt;td&gt;No user-agent-based block on this URL&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;
&lt;code&gt;403&lt;/code&gt; with a &lt;code&gt;cf-ray&lt;/code&gt; header (Cloudflare adds it to responses, see &lt;a href="https://developers.cloudflare.com/fundamentals/reference/http-headers/" rel="noopener noreferrer"&gt;HTTP headers&lt;/a&gt;)&lt;/td&gt;
&lt;td&gt;Blocked at the edge: AI Crawl Control, an AI bot policy, or a custom rule&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;402&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;AI Crawl Control block configured as "Payment Required"&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;
&lt;code&gt;cf-mitigated: challenge&lt;/code&gt; header&lt;/td&gt;
&lt;td&gt;A Cloudflare challenge page; per &lt;a href="https://developers.cloudflare.com/cloudflare-challenges/challenge-types/challenge-pages/detect-response/" rel="noopener noreferrer"&gt;the docs&lt;/a&gt; this header is set on every challenge page type&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;429&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Rate limiting&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;
&lt;code&gt;200&lt;/code&gt; but robots.txt disallows the bot&lt;/td&gt;
&lt;td&gt;No enforcement, just a stated preference that compliant crawlers follow&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;&lt;strong&gt;An important limit:&lt;/strong&gt; your &lt;code&gt;curl&lt;/code&gt; comes from your IP with a borrowed user agent. Cloudflare verifies real bots by other means: Web Bot Auth signatures, published IP lists, or reverse DNS (&lt;a href="https://developers.cloudflare.com/bots/concepts/bot/verified-bots/" rel="noopener noreferrer"&gt;Verified bots&lt;/a&gt;). So this test catches user-agent-based rules (including AI Crawl Control on the Free plan), but it can't perfectly reproduce how verified bot traffic is treated, in either direction. Confirm in the dashboard: &lt;strong&gt;Security &amp;gt; Analytics &amp;gt; Events&lt;/strong&gt; shows which service acted on a request, and the AI Crawl Control &lt;strong&gt;Crawlers&lt;/strong&gt; tab shows requests and unsuccessful requests per crawler.&lt;/p&gt;

&lt;h2&gt;
  
  
  Choosing deliberately
&lt;/h2&gt;

&lt;p&gt;A reasonable starting point for a business that wants to be found in AI answers:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Search: Allow.&lt;/strong&gt; This is how &lt;code&gt;OAI-SearchBot&lt;/code&gt;, &lt;code&gt;Claude-SearchBot&lt;/code&gt; and &lt;code&gt;PerplexityBot&lt;/code&gt; build what they cite.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Agent: think before blocking.&lt;/strong&gt; Cloudflare's Agent category includes chat fetch bots, the kind that open your page when someone asks about you.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Training: your call.&lt;/strong&gt; Disallow AI Training keeps Accountable mixed-use crawlers for search. Plain Block now affects Googlebot and Bingbot as well.&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  Common mistakes
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Testing only in a browser.&lt;/strong&gt; Humans and crawlers can get different answers from the same URL.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Treating robots.txt as a firewall.&lt;/strong&gt; It's a preference. Cloudflare's own docs say so.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Choosing "Block" for Training to "just stop AI".&lt;/strong&gt; After September 15, that also stops Googlebot and Bingbot.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Forgetting Bot Fight Mode.&lt;/strong&gt; Your WAF allow rule won't override it.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Allowing by user agent alone.&lt;/strong&gt; Use &lt;code&gt;cf.client.bot&lt;/code&gt; or IP ranges, or anyone can claim to be GPTBot.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;If you'd rather have this checked continuously, &lt;a href="https://aurageo.io/en/" rel="noopener noreferrer"&gt;AURA GEO&lt;/a&gt; audits what your server actually returns and keeps synthetic user-agent tests separate from real crawler evidence. There's a related explainer on &lt;a href="https://aurageo.io/en/learn/cdn-waf-blokiruet-ai-botov/" rel="noopener noreferrer"&gt;checking CDN and WAF rules that block AI bots&lt;/a&gt;.&lt;/p&gt;

</description>
      <category>cloudflare</category>
      <category>seo</category>
      <category>ai</category>
      <category>webdev</category>
    </item>
    <item>
      <title>Can AI Crawlers See Your JavaScript? A 10-Minute curl Test for the HTML Bots Actually Get</title>
      <dc:creator>AURA GEO</dc:creator>
      <pubDate>Thu, 22 Oct 2026 16:09:01 +0000</pubDate>
      <link>https://dev.to/aurageo/can-ai-crawlers-see-your-javascript-a-10-minute-curl-test-for-the-html-bots-actually-get-2j35</link>
      <guid>https://dev.to/aurageo/can-ai-crawlers-see-your-javascript-a-10-minute-curl-test-for-the-html-bots-actually-get-2j35</guid>
      <description>&lt;p&gt;Your page looks perfect in Chrome. Headline, pricing, FAQ, a nice comparison table. But an AI crawler doesn't open Chrome. It sends one HTTP request, gets back whatever your server returns, and moves on.&lt;/p&gt;

&lt;p&gt;If your important text only appears after JavaScript runs, there's a real chance that crawler saw an empty shell. This post shows how to check that in about ten minutes with &lt;code&gt;curl&lt;/code&gt; and a couple of one-liners.&lt;/p&gt;

&lt;h2&gt;
  
  
  Why this matters for AI search
&lt;/h2&gt;

&lt;p&gt;There are two versions of every page:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;The initial HTML&lt;/strong&gt; - what the server sends in response to a request.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;The rendered page&lt;/strong&gt; - what you see after scripts run, APIs return data and widgets load.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;Google documents a separate rendering stage for JavaScript (&lt;a href="https://developers.google.com/search/docs/crawling-indexing/javascript/javascript-seo-basics" rel="noopener noreferrer"&gt;JavaScript SEO basics&lt;/a&gt;). Other clients don't have to copy Google's capabilities. In December 2024, Vercel and MERJ published an analysis of AI crawler traffic on Vercel's network (&lt;a href="https://vercel.com/blog/the-rise-of-the-ai-crawler" rel="noopener noreferrer"&gt;The rise of the AI crawler&lt;/a&gt;). They found that OpenAI's crawlers, ClaudeBot, PerplexityBot and several others fetched JavaScript files but didn't execute them, while Gemini (via Google's infrastructure) and Applebot did render pages. Crawler behavior can change over time, so treat this as a reason to test, not as a permanent rule.&lt;/p&gt;

&lt;p&gt;The safe assumption: &lt;strong&gt;if a fact isn't in the initial HTML, don't count on an AI crawler seeing it.&lt;/strong&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  Step 1: Pick three pages, not your whole site
&lt;/h2&gt;

&lt;p&gt;Don't start with a full crawl. Choose:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;a service or pricing page,&lt;/li&gt;
&lt;li&gt;one article or guide,&lt;/li&gt;
&lt;li&gt;one product or listing page.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;For each one, write down 3-5 facts that must be visible: the H1, the main description, a price or condition, a key claim, and the important internal links.&lt;/p&gt;

&lt;h2&gt;
  
  
  Step 2: Fetch the raw HTML
&lt;/h2&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="nv"&gt;URL&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="s2"&gt;"https://example.com/pricing"&lt;/span&gt;

curl &lt;span class="nt"&gt;-sL&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="nv"&gt;$URL&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt; &lt;span class="nt"&gt;-o&lt;/span&gt; raw.html
&lt;span class="nb"&gt;wc&lt;/span&gt; &lt;span class="nt"&gt;-c&lt;/span&gt; raw.html
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Ignore the file size. A 300 KB response can be all navigation and inline JSON; a 20 KB response can contain the full answer. What matters is meaning.&lt;/p&gt;

&lt;p&gt;To see the response a specific crawler would get, you can also send its user agent string (useful when a CDN or WAF treats bots differently):&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;curl &lt;span class="nt"&gt;-sL&lt;/span&gt; &lt;span class="nt"&gt;-A&lt;/span&gt; &lt;span class="s2"&gt;"Mozilla/5.0 AppleWebKit/537.36 (KHTML, like Gecko; compatible; GPTBot/1.1; +https://openai.com/gptbot)"&lt;/span&gt; &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;-o&lt;/span&gt; raw_gptbot.html &lt;span class="nt"&gt;-w&lt;/span&gt; &lt;span class="s2"&gt;"%{http_code}&lt;/span&gt;&lt;span class="se"&gt;\n&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="nv"&gt;$URL&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;If this returns &lt;code&gt;403&lt;/code&gt;, &lt;code&gt;429&lt;/code&gt; or a challenge page while a normal request returns &lt;code&gt;200&lt;/code&gt;, you have a different problem: your firewall, not your rendering. Fix that first.&lt;/p&gt;

&lt;h2&gt;
  
  
  Step 3: Search for your must-have facts
&lt;/h2&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="k"&gt;for &lt;/span&gt;phrase &lt;span class="k"&gt;in&lt;/span&gt; &lt;span class="s2"&gt;"Pricing"&lt;/span&gt; &lt;span class="s2"&gt;"per month"&lt;/span&gt; &lt;span class="s2"&gt;"Free trial"&lt;/span&gt; &lt;span class="s2"&gt;"How it works"&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="k"&gt;do
  if &lt;/span&gt;&lt;span class="nb"&gt;grep&lt;/span&gt; &lt;span class="nt"&gt;-qi&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="nv"&gt;$phrase&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt; raw.html&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="k"&gt;then
    &lt;/span&gt;&lt;span class="nb"&gt;echo&lt;/span&gt; &lt;span class="s2"&gt;"FOUND:   &lt;/span&gt;&lt;span class="nv"&gt;$phrase&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt;
  &lt;span class="k"&gt;else
    &lt;/span&gt;&lt;span class="nb"&gt;echo&lt;/span&gt; &lt;span class="s2"&gt;"MISSING: &lt;/span&gt;&lt;span class="nv"&gt;$phrase&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt;
  &lt;span class="k"&gt;fi
done&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;A match isn't the whole story. Text sitting inside a JSON blob in a &lt;code&gt;&amp;lt;script&amp;gt;&lt;/code&gt; tag (for example &lt;code&gt;__NEXT_DATA__&lt;/code&gt; or a hydration payload) is technically "in the HTML," but a normal content extractor may not treat it as the main content. (The Vercel study notes that models can sometimes interpret JSON in the initial response, but you shouldn't rely on it.) Check where the match lives:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="nb"&gt;grep&lt;/span&gt; &lt;span class="nt"&gt;-o&lt;/span&gt; &lt;span class="nt"&gt;-i&lt;/span&gt; &lt;span class="s2"&gt;".&lt;/span&gt;&lt;span class="se"&gt;\{&lt;/span&gt;&lt;span class="s2"&gt;80&lt;/span&gt;&lt;span class="se"&gt;\}&lt;/span&gt;&lt;span class="s2"&gt;per month.&lt;/span&gt;&lt;span class="se"&gt;\{&lt;/span&gt;&lt;span class="s2"&gt;80&lt;/span&gt;&lt;span class="se"&gt;\}&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt; raw.html | &lt;span class="nb"&gt;head&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;If every hit is inside a script, mark it as "present in data, not in markup."&lt;/p&gt;

&lt;h2&gt;
  
  
  Step 4: Strip the markup and read it like a bot
&lt;/h2&gt;

&lt;p&gt;A quick way to see the visible text without a browser:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;python3 - &lt;span class="o"&gt;&amp;lt;&amp;lt;&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="no"&gt;PY&lt;/span&gt;&lt;span class="sh"&gt;'
import re, html
t = open("raw.html", encoding="utf-8", errors="ignore").read()
t = re.sub(r"(?is)&amp;lt;(script|style|noscript|svg)[^&amp;gt;]*&amp;gt;.*?&amp;lt;/&lt;/span&gt;&lt;span class="se"&gt;\1&lt;/span&gt;&lt;span class="sh"&gt;&amp;gt;", " ", t)
t = re.sub(r"(?s)&amp;lt;[^&amp;gt;]+&amp;gt;", " ", t)
t = re.sub(r"&lt;/span&gt;&lt;span class="se"&gt;\s&lt;/span&gt;&lt;span class="sh"&gt;+", " ", html.unescape(t)).strip()
print(len(t.split()), "words")
print(t[:1500])
&lt;/span&gt;&lt;span class="no"&gt;PY
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Read the first 1,500 characters. Does it say what the page is about and what you offer? Or is it "Loading... Menu Sign in Cookie settings"?&lt;/p&gt;

&lt;h2&gt;
  
  
  Step 5: Compare with the rendered page
&lt;/h2&gt;

&lt;p&gt;Open the same URL in your browser, open DevTools and compare the rendered DOM with &lt;code&gt;raw.html&lt;/code&gt;. Note which important elements only exist after load: tabs, accordions filled by API calls, pricing tables from a widget, reviews from a third-party script.&lt;/p&gt;

&lt;p&gt;Also test &lt;strong&gt;direct URL access&lt;/strong&gt;. Single-page apps sometimes work when you click from the homepage but return a generic shell (or a 404) when the route is requested directly. Crawlers always request directly.&lt;/p&gt;

&lt;h2&gt;
  
  
  Step 6: Fix delivery, not the content
&lt;/h2&gt;

&lt;p&gt;If the useful text is missing from the initial HTML, the usual options are:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Server-side rendering (SSR)&lt;/strong&gt; for pages that change often.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Static generation / prerendering&lt;/strong&gt; for pages that change rarely.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Moving critical facts into the server-rendered template&lt;/strong&gt; (H1, intro, price, key FAQ answers) while keeping interactive extras client-side.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Don't build a separate "bot version" with different claims. Visitors and crawlers should get the same meaning; serving different content to bots is the kind of thing that backfires.&lt;/p&gt;

&lt;h2&gt;
  
  
  Step 7: Save before and after
&lt;/h2&gt;

&lt;p&gt;When the fix ships, re-run the same &lt;code&gt;curl&lt;/code&gt; and the same checks. Keep both files:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;curl &lt;span class="nt"&gt;-sL&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="nv"&gt;$URL&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt; &lt;span class="nt"&gt;-o&lt;/span&gt; &lt;span class="s2"&gt;"after_&lt;/span&gt;&lt;span class="si"&gt;$(&lt;/span&gt;&lt;span class="nb"&gt;date&lt;/span&gt; +%F&lt;span class="si"&gt;)&lt;/span&gt;&lt;span class="s2"&gt;.html"&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;A fix is done when the main text is present in a direct request, the page still works for humans, and the required links are there. That removes a technical barrier. It doesn't make the page relevant to every question or guarantee a citation, so track discovery and citations separately.&lt;/p&gt;

&lt;h2&gt;
  
  
  A quick checklist
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;[ ] Three representative pages chosen&lt;/li&gt;
&lt;li&gt;[ ] Must-have facts listed per page&lt;/li&gt;
&lt;li&gt;[ ] Raw HTML fetched (normal UA and crawler UA)&lt;/li&gt;
&lt;li&gt;[ ] Status codes match between the two&lt;/li&gt;
&lt;li&gt;[ ] Facts found in markup, not only in script data&lt;/li&gt;
&lt;li&gt;[ ] Direct URL access tested for app routes&lt;/li&gt;
&lt;li&gt;[ ] Fix verified with a before/after snapshot&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;If you want this check done automatically, &lt;a href="https://aurageo.io/en/" rel="noopener noreferrer"&gt;AURA GEO&lt;/a&gt; audits the HTML your server actually returns, flags pages where little readable text comes through, and turns that into tasks you can hand to your own agent over MCP. There's a longer explainer here: &lt;a href="https://aurageo.io/en/learn/html-javascript-i-chtenie-sajta-ii/" rel="noopener noreferrer"&gt;how to tell whether a crawler can see the text on a JavaScript website&lt;/a&gt;.&lt;/p&gt;

</description>
      <category>seo</category>
      <category>webdev</category>
      <category>ai</category>
      <category>javascript</category>
    </item>
    <item>
      <title>Is That Really GPTBot? How to Find and Verify AI Crawlers in Your Server Logs</title>
      <dc:creator>AURA GEO</dc:creator>
      <pubDate>Wed, 21 Oct 2026 16:14:49 +0000</pubDate>
      <link>https://dev.to/aurageo/is-that-really-gptbot-how-to-find-and-verify-ai-crawlers-in-your-server-logs-33m5</link>
      <guid>https://dev.to/aurageo/is-that-really-gptbot-how-to-find-and-verify-ai-crawlers-in-your-server-logs-33m5</guid>
      <description>&lt;p&gt;You've allowed AI search bots in &lt;code&gt;robots.txt&lt;/code&gt;. Great. But are they actually reaching your pages, and are the requests in your logs really from them?&lt;/p&gt;

&lt;p&gt;The &lt;code&gt;User-Agent&lt;/code&gt; header is just a string. Anyone can send &lt;code&gt;GPTBot&lt;/code&gt; in a curl request. So "I saw GPTBot in my logs" proves very little until you check two things: &lt;strong&gt;what status code the bot got&lt;/strong&gt; and &lt;strong&gt;whether the IP really belongs to the vendor&lt;/strong&gt;.&lt;/p&gt;

&lt;p&gt;This guide shows how to do both with standard tools: &lt;code&gt;grep&lt;/code&gt;, &lt;code&gt;awk&lt;/code&gt; and a short Python script.&lt;/p&gt;

&lt;h2&gt;
  
  
  1. Know which bots you're looking for
&lt;/h2&gt;

&lt;p&gt;Major AI vendors now separate bots by purpose. This matters, because blocking the wrong one can quietly remove you from AI search answers. According to the vendors' own documentation:&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Vendor&lt;/th&gt;
&lt;th&gt;Bot&lt;/th&gt;
&lt;th&gt;What it's for&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;OpenAI&lt;/td&gt;
&lt;td&gt;&lt;code&gt;OAI-SearchBot&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Surfacing sites in ChatGPT search results&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;OpenAI&lt;/td&gt;
&lt;td&gt;&lt;code&gt;GPTBot&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Crawling content that may be used for model training&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;OpenAI&lt;/td&gt;
&lt;td&gt;&lt;code&gt;ChatGPT-User&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Visits triggered by a user's request in ChatGPT&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Perplexity&lt;/td&gt;
&lt;td&gt;&lt;code&gt;PerplexityBot&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Surfacing and linking sites in Perplexity search&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Perplexity&lt;/td&gt;
&lt;td&gt;&lt;code&gt;Perplexity-User&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Fetches triggered by a user's question&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Anthropic&lt;/td&gt;
&lt;td&gt;&lt;code&gt;Claude-SearchBot&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Improving Claude's search results&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Anthropic&lt;/td&gt;
&lt;td&gt;&lt;code&gt;ClaudeBot&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Collecting content that may be used for training&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Anthropic&lt;/td&gt;
&lt;td&gt;&lt;code&gt;Claude-User&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Fetches when a user asks Claude a question&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;Two practical notes from the docs:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;OpenAI says sites opted out of &lt;code&gt;OAI-SearchBot&lt;/code&gt; won't be shown in ChatGPT search answers (except as navigational links), and that a &lt;code&gt;robots.txt&lt;/code&gt; change can take about 24 hours to take effect for search. Perplexity also mentions up to 24 hours.&lt;/li&gt;
&lt;li&gt;User-triggered fetchers (&lt;code&gt;ChatGPT-User&lt;/code&gt;, &lt;code&gt;Perplexity-User&lt;/code&gt;) are documented as acting on behalf of a user, so &lt;code&gt;robots.txt&lt;/code&gt; rules may not apply to them in the same way.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Sources: &lt;a href="https://platform.openai.com/docs/bots" rel="noopener noreferrer"&gt;OpenAI crawlers&lt;/a&gt;, &lt;a href="https://docs.perplexity.ai/guides/bots" rel="noopener noreferrer"&gt;Perplexity crawlers&lt;/a&gt;, &lt;a href="https://support.claude.com/en/articles/8896518-does-anthropic-crawl-data-from-the-web-and-how-can-site-owners-block-the-crawler" rel="noopener noreferrer"&gt;Anthropic crawlers&lt;/a&gt;.&lt;/p&gt;

&lt;h2&gt;
  
  
  2. Find AI bot hits in your access log
&lt;/h2&gt;

&lt;p&gt;The examples assume the standard nginx/Apache "combined" log format, where the client IP is the first field, the status code is the ninth, and the user agent is the sixth field when you split on double quotes.&lt;/p&gt;

&lt;p&gt;Count requests per bot:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="nb"&gt;grep&lt;/span&gt; &lt;span class="nt"&gt;-oE&lt;/span&gt; &lt;span class="s1"&gt;'OAI-SearchBot|GPTBot|ChatGPT-User|PerplexityBot|Perplexity-User|Claude-SearchBot|ClaudeBot|Claude-User'&lt;/span&gt; &lt;span class="se"&gt;\&lt;/span&gt;
  /var/log/nginx/access.log | &lt;span class="nb"&gt;sort&lt;/span&gt; | &lt;span class="nb"&gt;uniq&lt;/span&gt; &lt;span class="nt"&gt;-c&lt;/span&gt; | &lt;span class="nb"&gt;sort&lt;/span&gt; &lt;span class="nt"&gt;-rn&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;See which pages a specific bot requested:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="nb"&gt;grep&lt;/span&gt; &lt;span class="s1"&gt;'OAI-SearchBot'&lt;/span&gt; /var/log/nginx/access.log &lt;span class="se"&gt;\&lt;/span&gt;
  | &lt;span class="nb"&gt;awk&lt;/span&gt; &lt;span class="s1"&gt;'{print $7}'&lt;/span&gt; | &lt;span class="nb"&gt;sort&lt;/span&gt; | &lt;span class="nb"&gt;uniq&lt;/span&gt; &lt;span class="nt"&gt;-c&lt;/span&gt; | &lt;span class="nb"&gt;sort&lt;/span&gt; &lt;span class="nt"&gt;-rn&lt;/span&gt; | &lt;span class="nb"&gt;head&lt;/span&gt; &lt;span class="nt"&gt;-20&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;If your logs are rotated and gzipped, use &lt;code&gt;zgrep&lt;/code&gt; on &lt;code&gt;access.log.*.gz&lt;/code&gt; the same way.&lt;/p&gt;

&lt;h2&gt;
  
  
  3. Check the status codes — this is where problems hide
&lt;/h2&gt;

&lt;p&gt;A bot that "visits" but receives &lt;code&gt;403&lt;/code&gt; or &lt;code&gt;429&lt;/code&gt; isn't reading your content. This is very common on sites behind a CDN or WAF with aggressive bot protection.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="nb"&gt;grep&lt;/span&gt; &lt;span class="nt"&gt;-E&lt;/span&gt; &lt;span class="s1"&gt;'OAI-SearchBot|PerplexityBot|Claude-SearchBot'&lt;/span&gt; /var/log/nginx/access.log &lt;span class="se"&gt;\&lt;/span&gt;
  | &lt;span class="nb"&gt;awk&lt;/span&gt; &lt;span class="s1"&gt;'{print $9}'&lt;/span&gt; | &lt;span class="nb"&gt;sort&lt;/span&gt; | &lt;span class="nb"&gt;uniq&lt;/span&gt; &lt;span class="nt"&gt;-c&lt;/span&gt; | &lt;span class="nb"&gt;sort&lt;/span&gt; &lt;span class="nt"&gt;-rn&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;What to look for:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;200&lt;/strong&gt; on real content pages — good.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;301/302&lt;/strong&gt; — fine if they resolve to a 200; check that redirects don't loop or land on a login page.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;403&lt;/strong&gt; — usually a firewall or bot-protection rule. Perplexity's docs, for example, describe allowing their bots in Cloudflare or AWS WAF by combining a user-agent match with their published IP ranges.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;429&lt;/strong&gt; — rate limiting. Bots may back off and come back less often.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;5xx&lt;/strong&gt; — your server failed; the bot got nothing useful.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Important: if a CDN blocks a request at the edge, it may never reach your origin server logs at all. In that case, check the CDN's own firewall/security events too.&lt;/p&gt;

&lt;h2&gt;
  
  
  4. Verify the IP, not just the user agent
&lt;/h2&gt;

&lt;p&gt;OpenAI and Perplexity publish JSON files with the IP ranges their bots use:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;&lt;code&gt;https://openai.com/searchbot.json&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;&lt;code&gt;https://openai.com/gptbot.json&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;&lt;code&gt;https://openai.com/chatgpt-user.json&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;&lt;code&gt;https://www.perplexity.ai/perplexitybot.json&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;&lt;code&gt;https://www.perplexity.ai/perplexity-user.json&lt;/code&gt;&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Each file contains a &lt;code&gt;prefixes&lt;/code&gt; list with &lt;code&gt;ipv4Prefix&lt;/code&gt; or &lt;code&gt;ipv6Prefix&lt;/code&gt; entries. Here's a small script that checks IPs from your log against them:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="c1"&gt;#!/usr/bin/env python3
&lt;/span&gt;&lt;span class="sh"&gt;"""&lt;/span&gt;&lt;span class="s"&gt;verify_ai_bots.py — check which logged AI-bot IPs match official ranges.&lt;/span&gt;&lt;span class="sh"&gt;"""&lt;/span&gt;
&lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;ipaddress&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;json&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;re&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;sys&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;urllib&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;request&lt;/span&gt;

&lt;span class="n"&gt;SOURCES&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;OAI-SearchBot&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;https://openai.com/searchbot.json&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;GPTBot&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;https://openai.com/gptbot.json&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;ChatGPT-User&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;https://openai.com/chatgpt-user.json&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;PerplexityBot&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;https://www.perplexity.ai/perplexitybot.json&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;Perplexity-User&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;https://www.perplexity.ai/perplexity-user.json&lt;/span&gt;&lt;span class="sh"&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;def&lt;/span&gt; &lt;span class="nf"&gt;load_ranges&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;url&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
    &lt;span class="n"&gt;req&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;urllib&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;request&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nc"&gt;Request&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;url&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;headers&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;User-Agent&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;range-check&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;})&lt;/span&gt;
    &lt;span class="n"&gt;data&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;json&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;load&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;urllib&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;request&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;urlopen&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;req&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;timeout&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="mi"&gt;20&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt;
    &lt;span class="n"&gt;nets&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;[]&lt;/span&gt;
    &lt;span class="k"&gt;for&lt;/span&gt; &lt;span class="n"&gt;p&lt;/span&gt; &lt;span class="ow"&gt;in&lt;/span&gt; &lt;span class="n"&gt;data&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="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;prefixes&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;[]):&lt;/span&gt;
        &lt;span class="n"&gt;cidr&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;p&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="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;ipv4Prefix&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="ow"&gt;or&lt;/span&gt; &lt;span class="n"&gt;p&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="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;ipv6Prefix&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
        &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;cidr&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
            &lt;span class="n"&gt;nets&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;append&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;ipaddress&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;ip_network&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;cidr&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt;
    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="n"&gt;nets&lt;/span&gt;

&lt;span class="n"&gt;ranges&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="n"&gt;bot&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nf"&gt;load_ranges&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;url&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;for&lt;/span&gt; &lt;span class="n"&gt;bot&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;url&lt;/span&gt; &lt;span class="ow"&gt;in&lt;/span&gt; &lt;span class="n"&gt;SOURCES&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;items&lt;/span&gt;&lt;span class="p"&gt;()}&lt;/span&gt;
&lt;span class="n"&gt;seen&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;{}&lt;/span&gt;

&lt;span class="k"&gt;with&lt;/span&gt; &lt;span class="nf"&gt;open&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;sys&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;argv&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;],&lt;/span&gt; &lt;span class="n"&gt;errors&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;replace&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;as&lt;/span&gt; &lt;span class="n"&gt;log&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
    &lt;span class="k"&gt;for&lt;/span&gt; &lt;span class="n"&gt;line&lt;/span&gt; &lt;span class="ow"&gt;in&lt;/span&gt; &lt;span class="n"&gt;log&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
        &lt;span class="k"&gt;for&lt;/span&gt; &lt;span class="n"&gt;bot&lt;/span&gt; &lt;span class="ow"&gt;in&lt;/span&gt; &lt;span class="n"&gt;ranges&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
            &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;re&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;search&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sa"&gt;rf&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;\b&lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="n"&gt;re&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;escape&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;bot&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="s"&gt;\b&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;line&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
                &lt;span class="n"&gt;ip&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;line&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;split&lt;/span&gt;&lt;span class="p"&gt;()[&lt;/span&gt;&lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;
                &lt;span class="n"&gt;seen&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;setdefault&lt;/span&gt;&lt;span class="p"&gt;((&lt;/span&gt;&lt;span class="n"&gt;bot&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;ip&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt; &lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
                &lt;span class="n"&gt;seen&lt;/span&gt;&lt;span class="p"&gt;[(&lt;/span&gt;&lt;span class="n"&gt;bot&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;ip&lt;/span&gt;&lt;span class="p"&gt;)]&lt;/span&gt; &lt;span class="o"&gt;+=&lt;/span&gt; &lt;span class="mi"&gt;1&lt;/span&gt;

&lt;span class="nf"&gt;for &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;bot&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;ip&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt; &lt;span class="n"&gt;hits&lt;/span&gt; &lt;span class="ow"&gt;in&lt;/span&gt; &lt;span class="nf"&gt;sorted&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;seen&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;items&lt;/span&gt;&lt;span class="p"&gt;()):&lt;/span&gt;
    &lt;span class="n"&gt;addr&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;ipaddress&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;ip_address&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;ip&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="n"&gt;ok&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;any&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;addr&lt;/span&gt; &lt;span class="ow"&gt;in&lt;/span&gt; &lt;span class="n"&gt;net&lt;/span&gt; &lt;span class="k"&gt;for&lt;/span&gt; &lt;span class="n"&gt;net&lt;/span&gt; &lt;span class="ow"&gt;in&lt;/span&gt; &lt;span class="n"&gt;ranges&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="n"&gt;bot&lt;/span&gt;&lt;span class="p"&gt;])&lt;/span&gt;
    &lt;span class="nf"&gt;print&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sa"&gt;f&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt;OK  &lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt; &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;ok&lt;/span&gt; &lt;span class="k"&gt;else&lt;/span&gt; &lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt;FAKE?&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="s"&gt; &lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="n"&gt;bot&lt;/span&gt;&lt;span class="si"&gt;:&lt;/span&gt;&lt;span class="mi"&gt;16&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="s"&gt; &lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="n"&gt;ip&lt;/span&gt;&lt;span class="si"&gt;:&lt;/span&gt;&lt;span class="mi"&gt;40&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="s"&gt; &lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="n"&gt;hits&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="s"&gt; hits&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Run it:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;python3 verify_ai_bots.py /var/log/nginx/access.log
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Anything marked &lt;code&gt;FAKE?&lt;/code&gt; claims to be an AI bot but comes from an IP outside the published ranges. Don't panic: the ranges change over time, so re-download them before drawing conclusions. But if a "GPTBot" is hammering your site from a random hosting provider, it's very likely not OpenAI.&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;Note: if your server sits behind a reverse proxy or CDN, the first log field may be the proxy's IP. Make sure you log the real client IP (for example via &lt;code&gt;X-Forwarded-For&lt;/code&gt; / &lt;code&gt;CF-Connecting-IP&lt;/code&gt; with a trusted-proxy config).&lt;/p&gt;
&lt;/blockquote&gt;

&lt;h2&gt;
  
  
  5. For Googlebot and Bingbot: reverse DNS
&lt;/h2&gt;

&lt;p&gt;Google and Bing also publish IP lists (&lt;code&gt;googlebot.json&lt;/code&gt;, &lt;code&gt;bingbot.json&lt;/code&gt;), and both support the classic reverse-then-forward DNS check:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;host 66.249.66.1
&lt;span class="c"&gt;# → ...googlebot.com&lt;/span&gt;
host crawl-66-249-66-1.googlebot.com
&lt;span class="c"&gt;# → must resolve back to 66.249.66.1&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;If the reverse lookup doesn't end in a Google-owned domain (such as &lt;code&gt;googlebot.com&lt;/code&gt; or &lt;code&gt;google.com&lt;/code&gt;), or the forward lookup doesn't return the same IP, it isn't Googlebot. The same idea works for Bingbot with &lt;code&gt;search.msn.com&lt;/code&gt;.&lt;/p&gt;

&lt;h2&gt;
  
  
  6. What a verified visit does — and doesn't — prove
&lt;/h2&gt;

&lt;p&gt;A verified &lt;code&gt;OAI-SearchBot&lt;/code&gt; hit with a &lt;code&gt;200&lt;/code&gt; tells you one thing: the bot could fetch that page. It does &lt;strong&gt;not&lt;/strong&gt; prove that:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;the page is indexed in that assistant's search,&lt;/li&gt;
&lt;li&gt;the page will be cited in an answer,&lt;/li&gt;
&lt;li&gt;anyone will click through.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Those are separate stages, and each needs its own evidence: a saved answer that links to your page, and a real referral visit in your analytics. Treat bot logs as the first link in that chain, not the finish line.&lt;/p&gt;

&lt;h2&gt;
  
  
  Quick checklist
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;[ ] List AI bots in your logs and count hits per bot&lt;/li&gt;
&lt;li&gt;[ ] Check status codes for search bots — no 403/429 on public pages&lt;/li&gt;
&lt;li&gt;[ ] Check CDN/WAF events for edge blocks that never reach your origin&lt;/li&gt;
&lt;li&gt;[ ] Verify IPs against the official JSON ranges (re-download them regularly)&lt;/li&gt;
&lt;li&gt;[ ] Use reverse DNS for Googlebot and Bingbot&lt;/li&gt;
&lt;li&gt;[ ] Don't treat a crawl as a citation&lt;/li&gt;
&lt;/ul&gt;




&lt;p&gt;If you'd rather not script this by hand, &lt;a href="https://aurageo.io/en/" rel="noopener noreferrer"&gt;AURA GEO&lt;/a&gt; runs these accessibility checks for your site, keeps a crawl history for AI bots and shows real referral visits from AI assistants — and your own agent can pick up the fixes over MCP. There's also an English write-up on &lt;a href="https://aurageo.io/en/learn/vizit-ii-bota-ne-raven-citirovaniyu/" rel="noopener noreferrer"&gt;why a bot visit isn't a citation&lt;/a&gt;.&lt;/p&gt;

</description>
      <category>seo</category>
      <category>webdev</category>
      <category>ai</category>
      <category>devops</category>
    </item>
    <item>
      <title>How to Get Your Site Ready for AI Search: robots.txt, llms.txt, Schema and AI Bots — A Practical Checklist</title>
      <dc:creator>AURA GEO</dc:creator>
      <pubDate>Tue, 20 Oct 2026 17:07:16 +0000</pubDate>
      <link>https://dev.to/aurageo/kak-podghotovit-sait-k-ii-poisku-robotstxt-llmstxt-schema-i-ii-boty-praktichieskii-chieklist-6e0</link>
      <guid>https://dev.to/aurageo/kak-podghotovit-sait-k-ii-poisku-robotstxt-llmstxt-schema-i-ii-boty-praktichieskii-chieklist-6e0</guid>
      <description>&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.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fk3hs7pnfr9eqmd1hftcx.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.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fk3hs7pnfr9eqmd1hftcx.png" alt="AURA GEO — hero" width="800" height="500"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;More and more people skip the ten blue links and search directly in ChatGPT, Gemini, Perplexity or Claude. An assistant with web search finds pages on its own, reads them and builds an answer with links to its sources. If your site is blocked, slow or hard to parse for these systems, it won't make it into the answer — even if it ranks well on Google.&lt;/p&gt;

&lt;p&gt;Preparing a site for this kind of search is called &lt;strong&gt;GEO (Generative Engine Optimization)&lt;/strong&gt;. The good news: the technical foundation of GEO overlaps almost entirely with good SEO. The bad news: there are a few places where sites break all the time, and a standard SEO audit doesn't catch them.&lt;/p&gt;

&lt;p&gt;Below is a checklist you can get through in an evening. Every item can be checked with a terminal command or a couple of lines of code.&lt;/p&gt;

&lt;h2&gt;
  
  
  1. Allow AI bots in robots.txt
&lt;/h2&gt;

&lt;p&gt;The first thing any crawler does is read &lt;code&gt;robots.txt&lt;/code&gt;. Over the past few years, plenty of sites copied "protective" rules and accidentally locked out AI search bots.&lt;/p&gt;

&lt;p&gt;It's important to tell two kinds of bots apart:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Training bots&lt;/strong&gt; (e.g. &lt;code&gt;GPTBot&lt;/code&gt;, &lt;code&gt;ClaudeBot&lt;/code&gt;, &lt;code&gt;Google-Extended&lt;/code&gt;) collect data to train models.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Search and user-triggered bots&lt;/strong&gt; (e.g. &lt;code&gt;OAI-SearchBot&lt;/code&gt;, &lt;code&gt;ChatGPT-User&lt;/code&gt;, &lt;code&gt;PerplexityBot&lt;/code&gt;, &lt;code&gt;Claude-User&lt;/code&gt;) show up when an assistant is looking for an answer for a specific user. These are the ones that bring citations and clicks.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;You can block training and still allow search. Example:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;# AI search and user-triggered bots — allow
User-agent: OAI-SearchBot
Allow: /

User-agent: ChatGPT-User
Allow: /

User-agent: PerplexityBot
Allow: /

User-agent: Claude-User
Allow: /

# Classic search engines
User-agent: Googlebot
Allow: /

User-agent: Bingbot
Allow: /

User-agent: *
Allow: /
Disallow: /admin/

Sitemap: https://example.com/sitemap.xml
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Check: make sure the file returns a 200 and doesn't contain a blanket &lt;code&gt;Disallow: /&lt;/code&gt; for the agents you care about.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;curl &lt;span class="nt"&gt;-s&lt;/span&gt; &lt;span class="nt"&gt;-o&lt;/span&gt; /dev/null &lt;span class="nt"&gt;-w&lt;/span&gt; &lt;span class="s2"&gt;"%{http_code}&lt;/span&gt;&lt;span class="se"&gt;\n&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt; https://example.com/robots.txt
curl &lt;span class="nt"&gt;-s&lt;/span&gt; https://example.com/robots.txt | &lt;span class="nb"&gt;grep&lt;/span&gt; &lt;span class="nt"&gt;-iE&lt;/span&gt; &lt;span class="s2"&gt;"user-agent|disallow"&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h3&gt;
  
  
  Don't forget your CDN and WAF
&lt;/h3&gt;

&lt;p&gt;A common trap: &lt;code&gt;robots.txt&lt;/code&gt; allows everything, but Cloudflare or another WAF serves bots a 403 or a captcha. Check the server response with different User-Agents:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="k"&gt;for &lt;/span&gt;ua &lt;span class="k"&gt;in&lt;/span&gt; &lt;span class="s2"&gt;"OAI-SearchBot"&lt;/span&gt; &lt;span class="s2"&gt;"ChatGPT-User"&lt;/span&gt; &lt;span class="s2"&gt;"PerplexityBot"&lt;/span&gt; &lt;span class="s2"&gt;"Googlebot"&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="k"&gt;do
  &lt;/span&gt;&lt;span class="nv"&gt;code&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="si"&gt;$(&lt;/span&gt;curl &lt;span class="nt"&gt;-s&lt;/span&gt; &lt;span class="nt"&gt;-o&lt;/span&gt; /dev/null &lt;span class="nt"&gt;-w&lt;/span&gt; &lt;span class="s2"&gt;"%{http_code}"&lt;/span&gt; &lt;span class="nt"&gt;-A&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="nv"&gt;$ua&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt; https://example.com/&lt;span class="si"&gt;)&lt;/span&gt;
  &lt;span class="nb"&gt;echo&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="nv"&gt;$ua&lt;/span&gt;&lt;span class="s2"&gt; -&amp;gt; &lt;/span&gt;&lt;span class="nv"&gt;$code&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt;
&lt;span class="k"&gt;done&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;If anything isn't a 200, look at your bot-protection rules.&lt;/p&gt;

&lt;h2&gt;
  
  
  2. Serve text in HTML, not just via JavaScript
&lt;/h2&gt;

&lt;p&gt;Many AI crawlers don't execute JavaScript, or only do so in a limited way. If your content only appears after an SPA hydrates, the bot sees an empty &lt;code&gt;&amp;lt;div id="root"&amp;gt;&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;A simple check — download the page and see whether the main text is there:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;curl &lt;span class="nt"&gt;-s&lt;/span&gt; https://example.com/pricing &lt;span class="se"&gt;\&lt;/span&gt;
  | &lt;span class="nb"&gt;sed&lt;/span&gt; &lt;span class="nt"&gt;-e&lt;/span&gt; &lt;span class="s1"&gt;'s/&amp;lt;[^&amp;gt;]*&amp;gt;//g'&lt;/span&gt; | &lt;span class="nb"&gt;tr&lt;/span&gt; &lt;span class="nt"&gt;-s&lt;/span&gt; &lt;span class="s1"&gt;' \n'&lt;/span&gt; | &lt;span class="nb"&gt;head&lt;/span&gt; &lt;span class="nt"&gt;-c&lt;/span&gt; 1500
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;If you see prices, descriptions and the FAQ, you're good. If all you get is the menu and empty space, you need SSR, SSG or prerendering (Next.js, Nuxt, Astro, Remix, etc.).&lt;/p&gt;

&lt;h2&gt;
  
  
  3. Add llms.txt
&lt;/h2&gt;

&lt;p&gt;&lt;code&gt;llms.txt&lt;/code&gt; is a proposed format: a Markdown file at the site root that briefly explains what the site is and lists key pages with descriptions. It isn't a standard, and no major assistant officially promises to use it. But it's cheap, useful for agents and developers, and it forces you to put the essentials of your product into words.&lt;/p&gt;

&lt;p&gt;Minimal example:&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;# Example Inc.&lt;/span&gt;
&lt;span class="gt"&gt;
&amp;gt; Example is an online booking service for small yoga studios.&lt;/span&gt;
&lt;span class="gt"&gt;&amp;gt; Operates in Latvia and Lithuania, interface in Russian and English.&lt;/span&gt;

&lt;span class="gu"&gt;## Main pages&lt;/span&gt;
&lt;span class="p"&gt;-&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="nv"&gt;Pricing&lt;/span&gt;&lt;span class="p"&gt;](&lt;/span&gt;&lt;span class="sx"&gt;https://example.com/pricing&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;: three plans, free trial.
&lt;span class="p"&gt;-&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="nv"&gt;API docs&lt;/span&gt;&lt;span class="p"&gt;](&lt;/span&gt;&lt;span class="sx"&gt;https://example.com/docs/api&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;: REST API for client bookings.
&lt;span class="p"&gt;-&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="nv"&gt;About&lt;/span&gt;&lt;span class="p"&gt;](&lt;/span&gt;&lt;span class="sx"&gt;https://example.com/about&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;: team, contacts, company details.

&lt;span class="gu"&gt;## FAQ&lt;/span&gt;
&lt;span class="p"&gt;-&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="nv"&gt;FAQ&lt;/span&gt;&lt;span class="p"&gt;](&lt;/span&gt;&lt;span class="sx"&gt;https://example.com/faq&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;: refunds, payments, integrations.
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;A good live example is &lt;a href="https://aurageo.io/llms.txt" rel="noopener noreferrer"&gt;AURA GEO's llms.txt&lt;/a&gt;: besides the list of pages, it honestly states what the service does &lt;em&gt;not&lt;/em&gt; do (for instance, that it doesn't "index sites in AI directly"). Explicit limitations like this are useful — models have less to fill in on their own.&lt;/p&gt;

&lt;h2&gt;
  
  
  4. Schema.org markup (JSON-LD)
&lt;/h2&gt;

&lt;p&gt;Structured data helps search engines — and, indirectly, AI — understand who you are and what you sell. The minimum set for a SaaS or service:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;code&gt;Organization&lt;/code&gt; — name, logo, &lt;code&gt;sameAs&lt;/code&gt; with links to external profiles;&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;WebSite&lt;/code&gt; — site name;&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;SoftwareApplication&lt;/code&gt; or &lt;code&gt;Product&lt;/code&gt; — for the product and pricing;&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;FAQPage&lt;/code&gt; — for a Q&amp;amp;A block;&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;Article&lt;/code&gt; — for blog posts and knowledge-base articles.
&lt;/li&gt;
&lt;/ul&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight html"&gt;&lt;code&gt;&lt;span class="nt"&gt;&amp;lt;script &lt;/span&gt;&lt;span class="na"&gt;type=&lt;/span&gt;&lt;span class="s"&gt;"application/ld+json"&lt;/span&gt;&lt;span class="nt"&gt;&amp;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;@context&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;https://schema.org&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;@type&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;Organization&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;name&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;Example Inc.&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;url&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;https://example.com&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;logo&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;https://example.com/logo.png&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;sameAs&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="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;https://github.com/example&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;https://www.producthunt.com/products/example&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="nt"&gt;&amp;lt;/script&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 html"&gt;&lt;code&gt;&lt;span class="nt"&gt;&amp;lt;script &lt;/span&gt;&lt;span class="na"&gt;type=&lt;/span&gt;&lt;span class="s"&gt;"application/ld+json"&lt;/span&gt;&lt;span class="nt"&gt;&amp;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;@context&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;https://schema.org&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;@type&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;FAQPage&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;mainEntity&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="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;@type&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;Question&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;name&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;Is there a free plan?&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;acceptedAnswer&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="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;@type&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;Answer&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;text&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;Yes, you can connect one site for free.&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="nt"&gt;&amp;lt;/script&amp;gt;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The golden rule: markup must match the visible text. If your JSON-LD contains an FAQ that isn't on the page, that's worse than having no markup at all.&lt;/p&gt;

&lt;p&gt;Check it with Google's Rich Results Test or validator.schema.org.&lt;/p&gt;

&lt;h2&gt;
  
  
  5. Sitemap, canonical and noindex
&lt;/h2&gt;

&lt;p&gt;AI assistants with search lean heavily on the Google and Bing indexes. A page that isn't indexed has almost no chance of making it into an answer. Check:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="c"&gt;# sitemap is reachable&lt;/span&gt;
curl &lt;span class="nt"&gt;-s&lt;/span&gt; &lt;span class="nt"&gt;-o&lt;/span&gt; /dev/null &lt;span class="nt"&gt;-w&lt;/span&gt; &lt;span class="s2"&gt;"%{http_code}&lt;/span&gt;&lt;span class="se"&gt;\n&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt; https://example.com/sitemap.xml

&lt;span class="c"&gt;# no accidental noindex, and canonical points to itself&lt;/span&gt;
curl &lt;span class="nt"&gt;-s&lt;/span&gt; https://example.com/pricing | &lt;span class="nb"&gt;grep&lt;/span&gt; &lt;span class="nt"&gt;-iE&lt;/span&gt; &lt;span class="s1"&gt;'noindex|rel="canonical"'&lt;/span&gt;
curl &lt;span class="nt"&gt;-sI&lt;/span&gt; https://example.com/pricing | &lt;span class="nb"&gt;grep&lt;/span&gt; &lt;span class="nt"&gt;-i&lt;/span&gt; x-robots-tag
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Also go into Google Search Console and Bing Webmaster Tools and review the report on non-indexed pages. Bing matters especially: several AI assistants rely on its index.&lt;/p&gt;

&lt;h2&gt;
  
  
  6. IndexNow: report updates right away
&lt;/h2&gt;

&lt;p&gt;IndexNow is a protocol for notifying Bing, Yandex and other participating search engines about new or changed URLs. Google doesn't use it, but for Bing it's the fastest route.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="c"&gt;# the key must be available at https://example.com/&amp;lt;KEY&amp;gt;.txt&lt;/span&gt;
curl &lt;span class="nt"&gt;-X&lt;/span&gt; POST &lt;span class="s2"&gt;"https://api.indexnow.org/indexnow"&lt;/span&gt; &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;-H&lt;/span&gt; &lt;span class="s2"&gt;"Content-Type: application/json; charset=utf-8"&lt;/span&gt; &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;-d&lt;/span&gt; &lt;span class="s1"&gt;'{
    "host": "example.com",
    "key": "&amp;lt;KEY&amp;gt;",
    "urlList": ["https://example.com/pricing", "https://example.com/faq"]
  }'&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Keep in mind: a successful submission only means "notification received" — not "page indexed", and certainly not "AI is citing it". These are separate stages, and each is worth checking on its own.&lt;/p&gt;

&lt;h2&gt;
  
  
  7. Write pages that are easy to cite
&lt;/h2&gt;

&lt;p&gt;Technical work opens the door, but content is what gets cited. What works:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Answer in the first paragraph.&lt;/strong&gt; Question in the heading, a direct 2–3 sentence answer right below it.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Specifics.&lt;/strong&gt; Prices, timelines, countries, limitations, versions. "Free for one site" beats "affordable pricing".&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Verifiable facts.&lt;/strong&gt; Last-updated date, author, source of the numbers.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Structure.&lt;/strong&gt; H2/H3, lists, comparison tables.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;One page per question.&lt;/strong&gt; A "How much does X cost" page gets cited more than a paragraph buried in a giant landing page.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Don't pass off demo data as facts.&lt;/strong&gt; If your landing page has nice example numbers in an interface mockup, label them as examples — otherwise a model may repeat them as real.&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  8. Measure: bot logs and AI referrals
&lt;/h2&gt;

&lt;p&gt;Without measurement, GEO turns into guesswork. The minimum:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;Server logs.&lt;/strong&gt; Look for AI bot visits:
&lt;/li&gt;
&lt;/ol&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="nb"&gt;grep&lt;/span&gt; &lt;span class="nt"&gt;-iE&lt;/span&gt; &lt;span class="s2"&gt;"GPTBot|OAI-SearchBot|ChatGPT-User|PerplexityBot|ClaudeBot|Claude-User"&lt;/span&gt; &lt;span class="se"&gt;\&lt;/span&gt;
  /var/log/nginx/access.log | &lt;span class="nb"&gt;awk&lt;/span&gt; &lt;span class="s1"&gt;'{print $1, $7, $12}'&lt;/span&gt; | &lt;span class="nb"&gt;tail&lt;/span&gt; &lt;span class="nt"&gt;-50&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;ol&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;Referrers in analytics.&lt;/strong&gt; Visits come from domains like &lt;code&gt;chatgpt.com&lt;/code&gt;, &lt;code&gt;perplexity.ai&lt;/code&gt;, &lt;code&gt;gemini.google.com&lt;/code&gt;, &lt;code&gt;copilot.microsoft.com&lt;/code&gt;. Create a separate segment for them.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;Manual answer checks.&lt;/strong&gt; Ask your customers' neutral questions in fresh chats with search enabled, save the answer and the list of sources, and repeat after making changes. A single check proves nothing — the trend is what matters.&lt;/p&gt;&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;You can stitch all of this together with scripts, or keep it in one dashboard. For example, &lt;a href="https://aurageo.io/en/" rel="noopener noreferrer"&gt;AURA GEO&lt;/a&gt; runs a technical audit (robots.txt, accessible HTML, noindex, sitemap), counts recognized AI referrals, lets you hand tasks to your own AI agent via MCP, and submits updated pages through IndexNow. The basic plan for one site is free.&lt;/p&gt;

&lt;h2&gt;
  
  
  Final checklist
&lt;/h2&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Item&lt;/th&gt;
&lt;th&gt;How to check&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;robots.txt allows AI search bots&lt;/td&gt;
&lt;td&gt;
&lt;code&gt;curl robots.txt&lt;/code&gt;, grep for User-agent&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;WAF doesn't block bots&lt;/td&gt;
&lt;td&gt;
&lt;code&gt;curl -A "ChatGPT-User"&lt;/code&gt; → 200&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Text is in HTML without JS&lt;/td&gt;
&lt;td&gt;
&lt;code&gt;curl&lt;/code&gt; + strip tags&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;llms.txt at the root&lt;/td&gt;
&lt;td&gt;&lt;code&gt;curl /llms.txt&lt;/code&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;JSON-LD: Organization, FAQPage&lt;/td&gt;
&lt;td&gt;Rich Results Test&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Sitemap, canonical, no noindex&lt;/td&gt;
&lt;td&gt;grep + Search Console / Bing WMT&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;IndexNow configured&lt;/td&gt;
&lt;td&gt;200/202 response from the API&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Pages answer specific questions&lt;/td&gt;
&lt;td&gt;content review&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Bot logs and AI referrers are tracked&lt;/td&gt;
&lt;td&gt;log grep, analytics segment&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;h2&gt;
  
  
  FAQ
&lt;/h2&gt;

&lt;h3&gt;
  
  
  What is GEO in plain terms?
&lt;/h3&gt;

&lt;p&gt;GEO (Generative Engine Optimization) is preparing a site so that AI assistants with web search can find, read and cite its pages. SEO is about rankings in regular search results; GEO is about showing up in answers from ChatGPT, Gemini, Perplexity and others.&lt;/p&gt;

&lt;h3&gt;
  
  
  Should I block GPTBot to protect my content?
&lt;/h3&gt;

&lt;p&gt;That's your call. Blocking training bots (GPTBot, ClaudeBot, Google-Extended) doesn't affect search bots like OAI-SearchBot and ChatGPT-User. If you want citations, don't block search and user-triggered bots.&lt;/p&gt;

&lt;h3&gt;
  
  
  Is llms.txt required?
&lt;/h3&gt;

&lt;p&gt;No. It's an optional format, and major assistants don't officially promise to use it. But it's cheap, useful for AI agents and developers, and helps you describe your product clearly.&lt;/p&gt;

&lt;h3&gt;
  
  
  Does schema.org help you get into AI answers?
&lt;/h3&gt;

&lt;p&gt;There are no direct guarantees, but markup helps search engines understand the entities on a page, and AI search relies heavily on their indexes. The key is that markup matches the visible text.&lt;/p&gt;

&lt;h3&gt;
  
  
  Does submitting to IndexNow mean the page will show up in ChatGPT?
&lt;/h3&gt;

&lt;p&gt;No. IndexNow only notifies search engines. Indexing, a bot visit, a citation and a click are separate stages, and each needs to be checked separately.&lt;/p&gt;

&lt;h3&gt;
  
  
  How do I know AI is already sending traffic?
&lt;/h3&gt;

&lt;p&gt;Look for &lt;code&gt;chatgpt.com&lt;/code&gt;, &lt;code&gt;perplexity.ai&lt;/code&gt; and &lt;code&gt;gemini.google.com&lt;/code&gt; referrers in your analytics and AI bot visits in your logs. And regularly check how assistants answer your customers' questions.&lt;/p&gt;

</description>
      <category>seo</category>
      <category>ai</category>
      <category>webdev</category>
      <category>geo</category>
    </item>
  </channel>
</rss>
