<p>There's a comfortable fiction at the heart of most "AI visibility" tooling, and until last week Sjá told it too. The fiction goes: an answer engine fetched your page, read all of it, understood it, and will quote whatever it likes. Score the page for schema, for question headings, for an <code>llms.txt</code>, and you have measured your AI visibility.</p><p>Except that isn't what happens. A crawler asks <code>robots.txt</code> whether it may come in. Then it asks the server, and the server — or the CDN in front of it — may say something different. If it gets in, it doesn't read the page; it extracts the main content, keeps what fits a reading budget, and picks passages it can quote. And at the end of all that, a real user asks ChatGPT a real question, and your page either shows up in the answer or it doesn't.</p><p>Sjá 0.6.0 rebuilds the GEO category along that path. Three layers — access, content, outcome — fifteen checks instead of four, and roughly half the score under <strong>Improve AI visibility</strong>, which is where a fresh install now starts. Here's how each layer works, why it's built the way it is, and what it looks like on a real page.</p><h2>Access: may they come in, and does the door actually open?</h2><p>The first question is embarrassingly basic and almost nobody checks it: can the answer crawlers fetch this page at all?</p><p>Sjá already read <code>robots.txt</code> properly — every user-agent group, most-specific match wins, the way the crawlers themselves resolve it. 0.6.0 adds the check that uses it: the page's own path is tested for <code>OAI-SearchBot</code>, <code>ChatGPT-User</code>, <code>Claude-User</code> and <code>Perplexity-User</code>. Blocking one of these is a fail, weight 10. These are the crawlers that fetch pages to answer questions right now; block them and you are absent from answers today.</p><p>Blocking <code>GPTBot</code>, <code>ClaudeBot</code> or <code>CCBot</code> — the training crawlers — is a weight-zero note. That's a licensing decision, and a perfectly respectable one. It is not a visibility problem, and a tool that penalised you for it would be confusing two different things.</p><p>But <code>robots.txt</code> is advice. So Sjá now fetches the page a second time, with <code>OAI-SearchBot</code>'s real User-Agent, and looks at what comes back. This is the only way to see a CDN's "block AI bots" switch, a WAF rule, or a bot challenge:</p><pre><code class="language-text">  Reachable as an AI crawler          fail   weight 8
    Fetched as OAI-SearchBot: HTTP 200, but the body is a Cloudflare
    challenge ("Just a moment…", cf-mitigated: challenge).
    robots.txt allows this crawler; the edge does not.
</code></pre><p>Note the status: 200. A naïve check would have passed it. The challenge page is recognised by its fingerprints regardless of the status code, because a crawler that receives a challenge has received nothing.</p><p>The fix prompt for this check includes the <code>curl</code> commands that prove the fix — fetch with each crawler's User-Agent and look at what you get — because "I flipped the switch in the dashboard" and "the crawler can now read the page" are different claims.</p><p>One more access check, and it's the one that catches modern sites: <strong>content without JavaScript</strong>. AI crawlers fetch HTML and execute nothing. Fewer than fifty words in the HTML, plus the fingerprints of a client-rendered app — an empty <code>#root</code> or <code>#__next</code>, a <code>&lt;noscript&gt;</code> plea — is a fail. Your beautiful React marketing page may be, to ChatGPT, a blank sheet with a loading spinner.</p><h2>Content: what the model sees</h2><p>This is the layer I'm most pleased with, because it makes something visible that was always there and never shown.</p><p>Engines don't read pages. They extract. So every Sjá report now carries a panel called <strong>What the model sees</strong>: the page's text in reading order, with navigation, footers, cookie banners, sidebars and forms stripped the way a readability extractor strips them. The report tells you how many visible words were discarded as boilerplate — and on a lot of sites that number is a quiet shock.</p><p>Then it draws a line. At roughly 2,000 tokens (about four characters each — stated as an estimate, because it is one) there's a marker, and every block of text knows whether it starts inside the budget or after it. Your best paragraph, the one that actually answers the question, may well be sitting below the line.</p><p>And then it picks passages. The five most quotable, each scored 0–100, with the reasons on both sides:</p><pre><code class="language-text">  Passage 2 · score 78 · starts inside budget
    "Askr embeds PHP in-process and, in worker mode, boots the
     application once and serves many requests. On a 4-core VM that
     removes roughly 30 ms of framework bootstrap per request."

    + 47 words, stands alone, full sentence, carries a figure
    + directly under a heading
    − heading is not a question
</code></pre><p>Forty to ninety words. Stands alone — doesn't open with "This" or "It" or "However" (or <em>Dette</em> and <em>Derfor</em>, since a lot of our users write Norwegian). Sits directly under a heading, doubly so if the heading is a question. Carries a figure or a date. Starts inside the budget.</p><p>The reasons on the minus side are the point. The gaps are what a writer changes. Turn the heading into the question the passage answers, move the passage above the fold, and you've done GEO — not by adding a schema block, but by writing the thing an engine can quote.</p><p>Three checks fall out of this: <strong>Quotable passages</strong>, <strong>Answer within the reading budget</strong>, and <strong>Signal to noise</strong>. And the MCP server and the CLI return the same extraction, so an assistant sitting in your editor edits the exact text the engine will see.</p><h3>Structured data, read rather than counted</h3><p>The old GEO had a check that counted <code>@type</code>s. 0.6.0 reads them for what an engine needs.</p><p>A block that isn't valid JSON used to register as "no structured data." Now it's a fail that says so — same outcome for the engine, entirely different fix. Required properties per type fail when missing (an <code>Article</code> without a <code>headline</code>, a <code>HowTo</code> without steps); recommended ones warn. <code>FAQPage</code> is checked question by question: a <code>Question</code> without a <code>name</code> or an answer text is called out individually.</p><p>Two things engines increasingly lean on, and that almost no tool checks: the publisher entity — is there an <code>Organization</code> behind this page, with a name, a canonical URL, a logo, <code>sameAs</code> links? Or only a <code>WebSite</code>, or nothing? A crawl now lists every distinct <code>Organization</code> name a site claims to be, with page counts, which is a gentle way of discovering that your site says it's three different companies. And dates that agree: <code>datePublished</code>, <code>dateModified</code>, <code>article:*</code> meta, the first <code>&lt;time datetime&gt;</code>, the <code>Last-Modified</code> header — collected and held to consistency. Modified before published is a fail. Two modified dates that disagree, a date in the future, a date over two years old: warnings. No dates at all is a note, weight zero — not everything has a date, and inventing one would be worse.</p><h2>Outcome: don't predict. Ask.</h2><p>Everything above is a prediction. Careful, deterministic, explainable — but a prediction. The last layer asks the engines directly.</p><p>From a page's report, type the question a user would actually ask — <em>what's the fastest way to run Laravel without PHP-FPM?</em> — and press <strong>Probe</strong>. Sjá puts it to ChatGPT (Responses API with web search), Claude (Messages API with web search) and Perplexity, concurrently, with your own API keys, and judges each answer:</p><pre><code class="language-text">  Citation probe · "fastest way to run Laravel without PHP-FPM"

    ChatGPT      cited     position 2 of 5
    Claude       cited     position 1 of 3
    Perplexity   not cited
                 cited instead: frankenphp.dev, roadrunner.dev, laravel.com
</code></pre><p>Does it cite your site? Where among the sources? And — this is the column you'll keep looking at — who was cited instead. That's your competitor list, drawn from the only source that matters.</p><p>Every probe is stored, failures included, so a gap in the history is visible as a gap rather than papered over. <strong>Trends</strong> shows the latest verdict per page and engine with the earlier ones as dots.</p><p>We were careful about what to say around this. The UI tells you plainly what a probe is: one sample of a non-deterministic system, through APIs that are close to — but not identical with — the consumer products, costing a few cents per engine on your own account. Ask the same question twice and you may get two different answers; that's the system, not a bug. What you should read is the trend over weeks and the competitor list, not one Tuesday's result.</p><p>The probe needs keys, one per engine, under <strong>Citation probe</strong> in <strong>Settings</strong>. An empty OpenAI or Anthropic key falls back to the key you already gave the AI agent, so nothing gets pasted twice.</p><h3>And did they actually come?</h3><p>Sjá could already read your nginx or Apache access log and tell you which AI crawlers really visited. 0.6.0 gives that a time axis: visits per crawler per day, stored per log file so two servers' logs add up and re-analysing a file replaces its days. <em>Did ClaudeBot come back after we changed robots.txt?</em> is now a chart, not a guess.</p><p>And because a User-Agent is a string anyone can send, there's <strong>Verify addresses</strong>. Sjá downloads the IP ranges each operator publishes — OpenAI's three crawlers, Googlebot, Bingbot, PerplexityBot, Applebot — and checks whether every visit claiming to be <code>GPTBot</code> came from OpenAI's ranges, some did (with the offending addresses), or none did. Operators that publish no list, Anthropic among them, are reported as unverifiable — never, ever as verified. The lists are cached locally for a week. No address from your log leaves your machine, because an access log is full of your visitors' IP addresses and that stays true in every feature we build.</p><h2>Why scores will move</h2><p>We should say this plainly, because it's the kind of thing that generates support email: your scores will change between 0.5.1 and 0.6.0.</p><p>GEO goes from 24 weight points to 80. Pages the crawlers can reach and read will go up. Pages behind a bot-blocking CDN, a JavaScript shell, a broken schema block or a missing publisher entity will go down — sometimes a lot. Compare runs across the upgrade with that in mind.</p><p>This is the point. A score that stayed stable while the tool started measuring the right things would be a score that wasn't measuring anything.</p><h2>Try it on your own site</h2><p>Update (0.4.2 or later updates itself; two small migrations run on first launch), open a page you care about, and look at three things:</p><ol><li><p><strong>Reachable as an AI crawler.</strong> If it's red on a 200, you've found the CDN switch nobody remembers flipping.</p></li><li><p><strong>What the model sees.</strong> Where's the budget line, and what's below it?</p></li><li><p><strong>Probe the question you most want to be the answer to.</strong> Then look at who was cited instead.</p></li></ol><p>Open the report. Read the footnotes.</p>