<?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: Александр Тригуб (GuruSEO)</title>
    <description>The latest articles on DEV Community by Александр Тригуб (GuruSEO) (@guruseo).</description>
    <link>https://dev.to/guruseo</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.amazonaws.com%2Fuploads%2Fuser%2Fprofile_image%2F3890813%2Fb16a585f-678e-44bb-a888-9d789d75c472.jpg</url>
      <title>DEV Community: Александр Тригуб (GuruSEO)</title>
      <link>https://dev.to/guruseo</link>
    </image>
    <atom:link rel="self" type="application/rss+xml" href="https://dev.to/feed/guruseo"/>
    <language>en</language>
    <item>
      <title>Is ChatGPT citing your site? A conceptual guide to GEO tracking in Python published</title>
      <dc:creator>Александр Тригуб (GuruSEO)</dc:creator>
      <pubDate>Tue, 21 Apr 2026 13:24:35 +0000</pubDate>
      <link>https://dev.to/guruseo/is-chatgpt-citing-your-site-a-conceptual-guide-to-geo-tracking-in-pythonpublished-53oj</link>
      <guid>https://dev.to/guruseo/is-chatgpt-citing-your-site-a-conceptual-guide-to-geo-tracking-in-pythonpublished-53oj</guid>
      <description>&lt;p&gt;When someone asks ChatGPT, Perplexity, or Gemini the same question, there is no rank. The model either mentions your site, paraphrases something from it, makes up a claim about it, or ignores it entirely. No dashboard tells you which happened.&lt;/p&gt;

&lt;p&gt;This is what people are starting to call GEO — Generative Engine Optimization. I've been building a tracker for it over the past several months, and I want to share the conceptual model that actually works, along with one minimal Python snippet you can run today.&lt;/p&gt;

&lt;p&gt;Fair warning: this is not a mature discipline. A lot of what's written about GEO online is vague hand-waving. I'll try to stay concrete about what I've verified and honest about what I haven't.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;What you're actually measuring&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;When someone asks an LLM "best family dentist in my city", there are four distinct outcomes for any given site — and they are not the same thing:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;1. Citation — the LLM names the site explicitly ("according to example.com...")&lt;/li&gt;
&lt;li&gt;Paraphrase without attribution — the LLM clearly ate your content but doesn't name you&lt;/li&gt;
&lt;li&gt;Distortion — the LLM mentions you, but gets something wrong: a price, a service you don't offer, an outdated fact&lt;/li&gt;
&lt;li&gt;Omission — the LLM ignores you entirely, even for queries where you should be a natural match&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;Classical SEO collapses everything into one metric (position). GEO needs at least four, and they trade off against each other. A site cited ten times with four distortions is in worse shape than a site cited three times correctly. If you only track citation rate, you miss that.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;The measurement loop&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;At its core, a GEO tracker does three things on a schedule:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;Send a query to an LLM&lt;/li&gt;
&lt;li&gt;Receive and parse the response&lt;/li&gt;
&lt;li&gt;Decide which of the four outcomes applies to your domain&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;Steps 1 and 2 are mechanical. Step 3 is where it gets hard — and that's where most of the design work lives.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;The minimal snippet&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;Here's the loop in its simplest form. I use OpenRouter because it gives you one API for many models, which you'll want later when you start comparing how different LLMs respond:&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;os&lt;/span&gt;
&lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;httpx&lt;/span&gt;

&lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;query_llm&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;prompt&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="n"&gt;model&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="o"&gt;-&amp;gt;&lt;/span&gt; &lt;span class="nb"&gt;str&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
    &lt;span class="sh"&gt;"""&lt;/span&gt;&lt;span class="s"&gt;Send a prompt, return the response text.&lt;/span&gt;&lt;span class="sh"&gt;"""&lt;/span&gt;
    &lt;span class="n"&gt;response&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;httpx&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;post&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://openrouter.ai/api/v1/chat/completions&lt;/span&gt;&lt;span class="sh"&gt;"&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;Authorization&lt;/span&gt;&lt;span class="sh"&gt;"&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;Bearer &lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="n"&gt;os&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;environ&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt;OPENROUTER_API_KEY&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="p"&gt;},&lt;/span&gt;
        &lt;span class="n"&gt;json&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;model&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;model&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
            &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;messages&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="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;role&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;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;content&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;prompt&lt;/span&gt;&lt;span class="p"&gt;}],&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;60&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="n"&gt;response&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;raise_for_status&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;response&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;json&lt;/span&gt;&lt;span class="p"&gt;()[&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;choices&lt;/span&gt;&lt;span class="sh"&gt;"&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="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;message&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;content&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;check_mention&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="nb"&gt;str&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;domain&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="o"&gt;-&amp;gt;&lt;/span&gt; &lt;span class="nb"&gt;dict&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
    &lt;span class="sh"&gt;"""&lt;/span&gt;&lt;span class="s"&gt;First-pass check: is the domain or brand in the response?&lt;/span&gt;&lt;span class="sh"&gt;"""&lt;/span&gt;
    &lt;span class="n"&gt;text_lower&lt;/span&gt; &lt;span class="o"&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;brand&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;domain&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;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="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;
    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;domain_mentioned&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;domain&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_lower&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;brand_mentioned&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;brand&lt;/span&gt; &lt;span class="ow"&gt;in&lt;/span&gt; &lt;span class="n"&gt;text_lower&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt;


&lt;span class="n"&gt;answer&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;query_llm&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
    &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;Who are the top independent SEO specialists in Russia?&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;deepseek/deepseek-chat&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="nf"&gt;print&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nf"&gt;check_mention&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;answer&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;example.com&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;That's the skeleton. Running it once for one query is not a tracker — it's a sanity check. Everything beyond this is making it smarter.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Where the real complexity lives&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;The snippet above gets you maybe 30% of the way. Here's what eats the rest:&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Query design.&lt;/strong&gt; One phrasing per topic is not enough. "Best X" vs "Recommend X" vs "Who should I hire for X" can produce completely different answer sets from the same model. I run five to fifteen variations per topic and aggregate. Without this you're measuring the LLM's reaction to one specific phrase, not the underlying landscape.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Model coverage.&lt;/strong&gt; ChatGPT, Claude, Gemini, Perplexity, and regional models like GigaChat or YandexGPT don't retrieve the same data the same way. Your site might be cited heavily by one and invisible in another. A single-model tracker tells you almost nothing — you need at least three in every cycle.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Distortion detection.&lt;/strong&gt; If the LLM cites you but misquotes a price or invents a service, that is often worse than being ignored. Detecting this means comparing the LLM's claim against your actual page content — a fuzzy-match problem with no clean solution. I currently combine phrase overlap, entity extraction, and a manual review queue for the ambiguous cases. It is not automated to my satisfaction yet.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Paraphrase without attribution.&lt;/strong&gt; The hardest case. The LLM obviously learned from your content but doesn't name you. Embedding similarity helps but is noisy. I lean on a combination of signals — phrase-level overlap, entity overlap, semantic similarity — and still end up reviewing edge cases by hand.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Stability.&lt;/strong&gt; LLMs are non-deterministic. Run the same query twice, get two different answers. One snapshot is not data. You need multiple runs per cycle and a statistical view — mean, variance, flip rate — not a single boolean.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Things I'd tell myself six months ago&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Don't build a dashboard first. Build a CSV-dumping script, run it for two weeks, stare at the raw output. The metrics that matter will emerge. If you lock in a schema early you'll be rebuilding it constantly.&lt;/li&gt;
&lt;li&gt;Distortion rate is more important than citation rate. Almost nobody tracks it. It's the metric with the highest ratio of business impact to effort required.&lt;/li&gt;
&lt;li&gt;Do not trust LLMs about their own sources. They will confidently invent URLs. Every claim has to be verified against the actual page, or you're building a tracker that measures hallucinations.&lt;/li&gt;
&lt;li&gt;Pick one vertical first. Trying to build a generic tracker across all niches at once will drown you in edge cases. Pick one domain you know well, get it working there, then generalize.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;strong&gt;Open questions&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;I do not have clean answers for these, and this is where I'd genuinely like input:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;How do you weight a mention in ChatGPT vs. Perplexity vs. Gemini? Does downstream traffic actually follow, or is it mostly brand signal?&lt;/li&gt;
&lt;li&gt;Is there a point where optimizing for LLM citation starts hurting classical SEO — e.g., by making content too "citation-bait" and lowering human engagement?&lt;/li&gt;
&lt;li&gt;What's the right cadence? Daily is overkill for most queries. Monthly misses fast shifts. Weekly feels right but I haven't validated it.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;If you've been working on anything in this space — tracking, experiments, methodology — I'd like to compare notes in the comments. This is early territory and I don't think anyone has it figured out yet.&lt;/p&gt;

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