<?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: SEO by Subham</title>
    <description>The latest articles on DEV Community by SEO by Subham (@seobysubham).</description>
    <link>https://dev.to/seobysubham</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%2F4090679%2F03f6e2a1-77cd-4b12-ba57-7e2bc24c2ac0.png</url>
      <title>DEV Community: SEO by Subham</title>
      <link>https://dev.to/seobysubham</link>
    </image>
    <atom:link rel="self" type="application/rss+xml" href="https://dev.to/feed/seobysubham"/>
    <language>en</language>
    <item>
      <title>The FAQ Rich Result You're Chasing Doesn't Exist Anymore</title>
      <dc:creator>SEO by Subham</dc:creator>
      <pubDate>Tue, 08 Sep 2026 08:52:35 +0000</pubDate>
      <link>https://dev.to/seobysubham/the-faq-rich-result-youre-chasing-doesnt-exist-anymore-160a</link>
      <guid>https://dev.to/seobysubham/the-faq-rich-result-youre-chasing-doesnt-exist-anymore-160a</guid>
      <description>&lt;p&gt;If you added FAQ schema to a page hoping for those little question dropdowns under your Google listing, I have some old news for you. That feature is gone.&lt;/p&gt;

&lt;p&gt;Not "harder to get." Not "restricted." Gone.&lt;/p&gt;

&lt;p&gt;Google first cut back FAQ rich results in August 2023, keeping them mostly for big government and health sites. Then on May 7, 2026, Google removed FAQ rich results from Search completely. Every site. No exceptions.&lt;/p&gt;

&lt;p&gt;A lot of SEO guides and plugin pages still haven't caught up on this. I kept seeing "FAQ schema shows extra answers in search results and boosts your click rate" written as if it's still true. It isn't. So before I wrote anything else, I wanted to figure out what FAQ schema actually does now, and if it's even worth using anymore.&lt;/p&gt;

&lt;p&gt;Short answer: yes, but not for the reason most people still think.&lt;/p&gt;

&lt;h2&gt;
  
  
  What FAQ Schema Actually Is
&lt;/h2&gt;

&lt;p&gt;FAQ schema is a small piece of code that tells search engines and other systems "this part of the page is a question, and this part right under it is the answer." It uses &lt;code&gt;FAQPage&lt;/code&gt;, &lt;code&gt;Question&lt;/code&gt;, and &lt;code&gt;Answer&lt;/code&gt; from Schema.org, written as JSON-LD. It sits in your page's &lt;code&gt;&amp;lt;head&amp;gt;&lt;/code&gt; and doesn't change anything visitors see.&lt;/p&gt;

&lt;p&gt;Say your page shows this on screen:&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;How long does SEO take to show results?&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;SEO results depend on your website, your competition, content quality, technical health, and how established your site already is. Some small changes show up fast. Bigger, more competitive changes take longer.&lt;/p&gt;

&lt;p&gt;The matching JSON-LD looks like this:&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="err"&gt;&amp;lt;script&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="err"&gt;type=&lt;/span&gt;&lt;span class="s2"&gt;"application/ld+json"&lt;/span&gt;&lt;span class="err"&gt;&amp;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;"@context"&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://schema.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;"@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;"FAQPage"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"mainEntity"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
      &lt;/span&gt;&lt;span class="nl"&gt;"@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;"Question"&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;"How long does SEO take to show results?"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
      &lt;/span&gt;&lt;span class="nl"&gt;"acceptedAnswer"&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;"Answer"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
        &lt;/span&gt;&lt;span class="nl"&gt;"text"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"SEO results depend on your website, your competition, content quality, technical health, and how established your site already is. Some small changes show up fast. Bigger, more competitive changes take longer."&lt;/span&gt;&lt;span class="w"&gt;
      &lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;span class="err"&gt;&amp;lt;/script&amp;gt;&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Quick breakdown of what each part does:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;code&gt;@context&lt;/code&gt; tells the parser which vocabulary to use (Schema.org)&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;@type: FAQPage&lt;/code&gt; marks this page as having FAQ content&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;mainEntity&lt;/code&gt; is the list of questions&lt;/li&gt;
&lt;li&gt;Each &lt;code&gt;Question&lt;/code&gt; has a &lt;code&gt;name&lt;/code&gt; (the question itself) and an &lt;code&gt;acceptedAnswer&lt;/code&gt; (the &lt;code&gt;Answer&lt;/code&gt; with the real text)&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Writing this by hand for one question is fine. Do it for ten questions and it gets messy fast. Missed commas and mismatched brackets are the usual problem.&lt;/p&gt;

&lt;h2&gt;
  
  
  So Why Bother If the Rich Result Is Gone?
&lt;/h2&gt;

&lt;p&gt;This is the part I had to actually think about instead of assuming.&lt;/p&gt;

&lt;p&gt;Google has said more than once that structured data helps it understand a page, even after a specific visual feature built on top of that data gets dropped. The rich result was just how that understanding showed up on screen. The information underneath it, "these questions and answers actually go together on this page", is still useful.&lt;/p&gt;

&lt;p&gt;And that matters more now than it did a few years ago. Search is moving toward AI summaries and answer tools that need to read and understand content, not just crawl it and rank it. Clean, clearly labeled Q&amp;amp;A helps with that. It's not a promise you'll get picked up anywhere. There's no code that buys you that. But clear structure removes guesswork, and guesswork is exactly what these systems struggle with.&lt;/p&gt;

&lt;p&gt;So the honest reason to still use FAQ schema in 2026 is this: it makes your page easier for machines to read correctly. It's not a shortcut to a Google feature that doesn't exist anymore.&lt;/p&gt;

&lt;h2&gt;
  
  
  I Built a Free Generator, and Left Out Auto-Extraction on Purpose
&lt;/h2&gt;

&lt;p&gt;Once I understood what FAQ schema is actually good for now, I built a small tool for it. You type in your questions and answers, watch a live preview update, then copy or download the finished code.&lt;/p&gt;

&lt;p&gt;One thing I chose not to add: auto-extraction from your page. I thought about it and decided against it, for the same reason my last two tools stayed manual.&lt;/p&gt;

&lt;p&gt;The most common way FAQ schema goes wrong is a mismatch. The code says one thing, the page shows another. A tool that guesses which headings are "questions" is a good way to create that mismatch without noticing. Typing in the exact questions and answers that are already on your page keeps things honest, since there's no guessing involved.&lt;/p&gt;

&lt;p&gt;The tool handles the fiddly part, the brackets, quotes, and correct JSON-LD formatting. The part that actually matters, whether this matches your real page, is still on you.&lt;/p&gt;

&lt;h2&gt;
  
  
  The Mistake That Matters More Than Getting the Code Right
&lt;/h2&gt;

&lt;p&gt;Perfectly formatted code can still describe something false.&lt;/p&gt;

&lt;p&gt;The biggest mistake isn't a missing comma. It's adding schema for questions that aren't really visible and answered on the page, usually to stuff in more keywords. Google's own guidelines say structured data should match real, visible content. Fake or hidden FAQ schema isn't just risky, it also doesn't get you anything anymore, since the reward it used to chase is gone.&lt;/p&gt;

&lt;p&gt;A smaller but common problem: duplicate schema. If your SEO plugin or theme already adds FAQPage markup, adding a second block by hand creates two conflicting pieces of code on the same page. Worth checking your page source before adding anything manually.&lt;/p&gt;

&lt;h2&gt;
  
  
  Where the Code Actually Goes
&lt;/h2&gt;

&lt;p&gt;The generated &lt;code&gt;&amp;lt;script type="application/ld+json"&amp;gt;&lt;/code&gt; block goes in the &lt;code&gt;&amp;lt;head&amp;gt;&lt;/code&gt; of the same page that shows the FAQ, not a different page, and not a global template unless that FAQ is genuinely on every page.&lt;/p&gt;

&lt;p&gt;A few notes depending on your platform:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;WordPress&lt;/strong&gt;: use your SEO plugin's custom schema field if it has one, or a trusted header tool. Don't paste raw JSON-LD into a normal text block, the visual editor can break it.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Shopify&lt;/strong&gt;: add it through the theme template's &lt;code&gt;&amp;lt;head&amp;gt;&lt;/code&gt; with Liquid, only on the specific page, not site-wide.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Wix&lt;/strong&gt;: use the custom code option to add it to that page's head, and check first that Wix hasn't already added its own FAQ markup.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Plain HTML&lt;/strong&gt;: drop the script tag straight into the &lt;code&gt;&amp;lt;head&amp;gt;&lt;/code&gt;, with the FAQ still showing normally as regular text on the page.&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  Try It
&lt;/h2&gt;

&lt;p&gt;If you have a page with a real FAQ section, the &lt;a href="https://seobysubham.com/faq-schema-generator" rel="noopener noreferrer"&gt;free FAQ Schema Generator&lt;/a&gt; turns it into clean FAQPage code with a live preview. No signup needed.&lt;/p&gt;

&lt;p&gt;Just don't add it expecting a fancy snippet in Google Search. That's gone. Add it because it helps machines read your content correctly. That's a smaller win than before, but it's a real one.&lt;/p&gt;

&lt;p&gt;If you've run into other SEO advice that's still stuck in the past, I'd like to hear about it. Drop it in the comments.&lt;/p&gt;

</description>
      <category>seo</category>
      <category>webdev</category>
      <category>javascript</category>
      <category>json</category>
    </item>
    <item>
      <title>Your Backlinks Are Indexed But Invisible to AI Search — I Built a Free Checker to Prove It</title>
      <dc:creator>SEO by Subham</dc:creator>
      <pubDate>Fri, 28 Aug 2026 00:31:23 +0000</pubDate>
      <link>https://dev.to/seobysubham/your-backlinks-are-indexed-but-invisible-to-ai-search-i-built-a-free-checker-to-prove-it-43p0</link>
      <guid>https://dev.to/seobysubham/your-backlinks-are-indexed-but-invisible-to-ai-search-i-built-a-free-checker-to-prove-it-43p0</guid>
      <description>&lt;h2&gt;
  
  
  The Backlink That Google Loved and ChatGPT Never Heard Of
&lt;/h2&gt;

&lt;p&gt;A few weeks back I published &lt;a href="https://dev.to/seobysubham/i-built-a-google-sheets-tool-to-check-backlink-indexation-with-serpapi-and-why-i-made-it-1h0k"&gt;a Google Sheets tool to check backlink indexation with SerpApi&lt;/a&gt; — a simple, manual-only script that tells you whether a backlink has actually been picked up by Google, instead of guessing.&lt;/p&gt;

&lt;p&gt;It worked well. One backlink I was tracking got indexed by Google in under 72 hours. Good news, right?&lt;/p&gt;

&lt;p&gt;Then, out of curiosity, I opened ChatGPT and asked it about the exact page that backlink pointed to.&lt;/p&gt;

&lt;p&gt;It had no idea the page existed.&lt;/p&gt;

&lt;p&gt;Not "I don't have real-time data." Not a vague guess. It simply didn't know the content was there — despite Google indexing it days earlier.&lt;/p&gt;

&lt;p&gt;That gap is the entire premise of this post: &lt;strong&gt;being indexed by Google and being visible to AI search are two completely different things&lt;/strong&gt;, and almost nobody is measuring the second one.&lt;/p&gt;

&lt;p&gt;So I extended my original checker to test for both.&lt;/p&gt;

&lt;h2&gt;
  
  
  Why "Indexed" Doesn't Mean "Visible" Anymore
&lt;/h2&gt;

&lt;p&gt;For over a decade, indexation was the finish line. If Google crawled your page and added it to the index, you were in the game — rankings were just a matter of time and optimization from there.&lt;/p&gt;

&lt;p&gt;That assumption is breaking down.&lt;/p&gt;

&lt;p&gt;A growing share of search now happens inside AI systems — ChatGPT, Perplexity, and Google's own AI Overviews — that don't work like the traditional index. They don't list ten blue links; they synthesize an answer from a handful of sources they trust enough to cite. A page can be perfectly indexed and still never make it into that shortlist.&lt;/p&gt;

&lt;p&gt;For anyone doing SEO or link building, that means a new failure mode nobody's watching for: a backlink that "worked" by every traditional metric, but contributes zero visibility where a growing number of searches actually happen.&lt;/p&gt;

&lt;p&gt;I wanted a way to see that gap directly, on my own sites, without relying on vibes.&lt;/p&gt;

&lt;h2&gt;
  
  
  What I Built: An Indexation + AI Visibility Checker
&lt;/h2&gt;

&lt;p&gt;The original tool answered one question per backlink: &lt;em&gt;is this indexed?&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;The updated version answers three:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;Is the backlink indexed by Google?&lt;/strong&gt; (unchanged from the original tool — SerpApi &lt;code&gt;site:&lt;/code&gt; query)&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Does the target page appear in Google's AI Overview&lt;/strong&gt; for the keywords it's meant to rank for?&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Does a general-purpose LLM (ChatGPT/Perplexity via API) recognize the page or domain at all&lt;/strong&gt; when asked directly?&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;Each backlink row in the Google Sheet ends up with three simple signals instead of one, plus a combined score so I can sort and prioritize at a glance.&lt;/p&gt;

&lt;h2&gt;
  
  
  The Technical Walkthrough
&lt;/h2&gt;

&lt;p&gt;Same stack as before: Python, the SerpApi library, and Google Sheets as the interface — no dashboard, no database, nothing to maintain.&lt;/p&gt;

&lt;h3&gt;
  
  
  1. Indexation check (recap)
&lt;/h3&gt;

&lt;p&gt;This part is unchanged from the original tool — a straightforward &lt;code&gt;site:&lt;/code&gt; search through SerpApi to confirm the URL is indexed:&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;requests&lt;/span&gt;

&lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;check_indexation&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;serpapi_key&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
    &lt;span class="n"&gt;params&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;engine&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&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;q&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;site:&lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="n"&gt;url&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="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;api_key&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;serpapi_key&lt;/span&gt;
    &lt;span class="p"&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;requests&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;https://serpapi.com/search&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;params&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="n"&gt;params&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="n"&gt;results&lt;/span&gt; &lt;span class="o"&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;json&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="nf"&gt;bool&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;results&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;organic_results&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;h3&gt;
  
  
  2. Google AI Overview check
&lt;/h3&gt;

&lt;p&gt;SerpApi's Google engine returns an &lt;code&gt;ai_overview&lt;/code&gt; block when one is present for a query. I check whether my domain shows up anywhere inside its sources:&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="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;check_ai_overview&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;keyword&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;target_domain&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;serpapi_key&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
    &lt;span class="n"&gt;params&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;engine&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&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;q&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;keyword&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;api_key&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;serpapi_key&lt;/span&gt;
    &lt;span class="p"&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;requests&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;https://serpapi.com/search&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;params&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="n"&gt;params&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="n"&gt;results&lt;/span&gt; &lt;span class="o"&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;json&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;

    &lt;span class="n"&gt;ai_overview&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;results&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;ai_overview&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;sources&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;ai_overview&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;references&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;cited&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;target_domain&lt;/span&gt; &lt;span class="ow"&gt;in&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;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;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="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;for&lt;/span&gt; &lt;span class="n"&gt;src&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="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;has_ai_overview&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nf"&gt;bool&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;ai_overview&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_cited&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;cited&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;If there's no AI Overview at all for that keyword yet, I record that too — it's a useful signal on its own.&lt;/p&gt;

&lt;h3&gt;
  
  
  3. Direct LLM recognition check
&lt;/h3&gt;

&lt;p&gt;This one is more experimental, and I want to be upfront about that. I send a small, consistent prompt to the OpenAI API asking whether the model has knowledge of the page or domain, and log the raw response for manual review rather than trying to auto-parse a yes/no:&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;from&lt;/span&gt; &lt;span class="n"&gt;openai&lt;/span&gt; &lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;OpenAI&lt;/span&gt;

&lt;span class="n"&gt;client&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nc"&gt;OpenAI&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;api_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;YOUR_OPENAI_KEY&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_llm_recognition&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;domain&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
    &lt;span class="n"&gt;prompt&lt;/span&gt; &lt;span class="o"&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;Are you aware of the website or page at &lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="n"&gt;url&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="sa"&gt;f&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;(domain: &lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="n"&gt;domain&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="s"&gt;)? If so, briefly describe what it&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt;s about. &lt;/span&gt;&lt;span class="sh"&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;If you have no knowledge of it, say so directly.&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;
    &lt;span class="p"&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;client&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;chat&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;completions&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;create&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
        &lt;span class="n"&gt;model&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;gpt-4o-mini&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="n"&gt;messages&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;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="n"&gt;max_tokens&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="mi"&gt;150&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="n"&gt;choices&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;message&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;content&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;I run the same prompt structure against Perplexity's API too, since its answers are grounded in live search rather than training data alone — it tells me something different: not "does the model remember this," but "would this page surface in a real-time AI search right now."&lt;/p&gt;

&lt;h3&gt;
  
  
  4. Combining it into one sheet
&lt;/h3&gt;

&lt;p&gt;Each backlink gets logged as a row with:&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Backlink URL&lt;/th&gt;
&lt;th&gt;Indexed?&lt;/th&gt;
&lt;th&gt;AI Overview exists?&lt;/th&gt;
&lt;th&gt;Domain cited in Overview?&lt;/th&gt;
&lt;th&gt;LLM recognizes page?&lt;/th&gt;
&lt;th&gt;Notes&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;I don't collapse this into a single fake "score out of 100" — SEO tools do that too often and it hides more than it reveals. Instead I flag any row where &lt;strong&gt;Indexed = Yes&lt;/strong&gt; but both AI columns are &lt;strong&gt;No&lt;/strong&gt;. Those are the interesting rows. That's the actual gap.&lt;/p&gt;

&lt;h2&gt;
  
  
  What I Found Running It
&lt;/h2&gt;

&lt;p&gt;Out of the backlinks I re-checked from my last batch:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Every single one was indexed by Google, as expected.&lt;/li&gt;
&lt;li&gt;Roughly a third appeared in an AI Overview for their target keyword.&lt;/li&gt;
&lt;li&gt;Almost none were recognized directly by the LLM when asked cold — which makes sense, since a backlink built last month isn't in any model's training data yet.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;The AI Overview number was the useful one. It's the closest thing to a real-time "does AI search actually see this" signal available right now, and it's a much smaller percentage than the indexation rate. That gap is exactly what I was trying to surface.&lt;/p&gt;

&lt;h2&gt;
  
  
  Why This Is Still Manual-Only
&lt;/h2&gt;

&lt;p&gt;Same reasoning as the original tool, and it applies even more here:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;API costs add up fast.&lt;/strong&gt; LLM calls aren't free, and neither are SerpApi credits. Automating this into a daily cron job for hundreds of URLs would get expensive quickly for very little marginal insight.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;AI answers aren't stable.&lt;/strong&gt; Ask the same question twice and you can get different phrasing, or a different verdict entirely. Automating on top of a non-deterministic signal just produces noisy automation, not a reliable pipeline.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;This is a diagnostic, not a dashboard.&lt;/strong&gt; I run it in batches when I want a checkpoint, review the raw answers myself, and move on. That's a better use of the signal than pretending it's precise enough to monitor continuously.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Manual-only isn't a limitation here — it's the correct way to use a signal that's still this noisy.&lt;/p&gt;

&lt;h2&gt;
  
  
  Limitations, Honestly
&lt;/h2&gt;

&lt;p&gt;A few things worth being upfront about, since overselling this would defeat the point of writing it:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;LLM "recognition" is not a real index. There's no API that tells you definitively what a model does or doesn't know — asking it directly is a proxy, not ground truth.&lt;/li&gt;
&lt;li&gt;AI Overviews don't appear for every query, and their presence/absence can shift between runs.&lt;/li&gt;
&lt;li&gt;None of this replaces actual traffic and conversion data. It's a leading indicator, not proof of impact.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;If you're building something similar, treat every AI-visibility signal as directional, not authoritative.&lt;/p&gt;

&lt;h2&gt;
  
  
  Try It Yourself
&lt;/h2&gt;

&lt;p&gt;The full script (indexation + AI Overview + LLM recognition check) is a single Python file plus a Google Sheet template — no server, no signup, nothing to host. If you want to run it on your own backlinks, I'm happy to share the setup in the comments or a follow-up post.&lt;/p&gt;

&lt;p&gt;If you've noticed the same indexed-but-invisible gap on your own sites, I'd genuinely like to hear what you found — drop it below.&lt;/p&gt;

&lt;p&gt;I write and build tools like this as part of my link-building work at &lt;a href="https://seobysubham.com" rel="noopener noreferrer"&gt;SEO by Subham&lt;/a&gt; — if you're curious about the manual, white-hat approach behind the original indexation checker too, it's all there.&lt;/p&gt;

</description>
      <category>seo</category>
      <category>python</category>
      <category>googlesheet</category>
      <category>webdev</category>
    </item>
    <item>
      <title>I built a Google Sheets tool to check backlink indexation with SerpApi, and why I made it manual-only</title>
      <dc:creator>SEO by Subham</dc:creator>
      <pubDate>Sun, 23 Aug 2026 10:40:55 +0000</pubDate>
      <link>https://dev.to/seobysubham/i-built-a-google-sheets-tool-to-check-backlink-indexation-with-serpapi-and-why-i-made-it-1h0k</link>
      <guid>https://dev.to/seobysubham/i-built-a-google-sheets-tool-to-check-backlink-indexation-with-serpapi-and-why-i-made-it-1h0k</guid>
      <description>&lt;p&gt;I do SEO and link-building work at &lt;a href="https://seobysubham.com/" rel="noopener noreferrer"&gt;seobysubham.com&lt;/a&gt;, and one task kept feeling unnecessarily repetitive: checking whether the pages containing backlinks were actually indexed by Google.&lt;/p&gt;

&lt;p&gt;The process is simple in theory. Take a list of backlink URLs, search for each one, and determine whether Google has the page in its index. Doing that manually for a large list is slow, especially when the URLs are already sitting in a Google Sheet. That was the main reason I decided to build the checker directly inside Sheets rather than as a separate app.&lt;/p&gt;

&lt;p&gt;I didn't want another small web application with its own login, database, hosting, and UI. The people using the data already had a spreadsheet. Putting the results there meant there was no extra interface to learn, no separate auth flow, and the rest of the team could see results in the same document they already worked from.&lt;/p&gt;

&lt;h2&gt;
  
  
  Why Google Apps Script?
&lt;/h2&gt;

&lt;p&gt;Apps Script was a natural fit because it sits directly inside Google Workspace. A spreadsheet can have an Apps Script project attached to it, and that script can read and write cells using the Sheets API available through the Apps Script runtime. It also lets me add a custom menu to the spreadsheet, so the checker starts from the UI instead of running as a completely separate application.&lt;/p&gt;

&lt;p&gt;The architecture is deliberately small:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Google Sheet
    |
    | Backlink URLs
    v
Apps Script
    |
    | HTTP request
    v
SerpApi
    |
    | Search response
    v
Apps Script
    |
    | Parse result
    v
Google Sheet
    |
    +-- Indexed
    +-- Not Indexed
    +-- Error
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;No dedicated server involved. The spreadsheet is effectively the interface, and Apps Script handles the application logic. Backlink URLs live in one column, indexation results get written into another.&lt;/p&gt;

&lt;h2&gt;
  
  
  The core logic
&lt;/h2&gt;

&lt;p&gt;The production version has more validation and error handling, but the basic implementation is fairly straightforward. For every backlink URL, the script sends a request to SerpApi. The response is JSON, so the script can inspect the returned search results and decide whether the target URL appears in Google's results.&lt;/p&gt;

&lt;p&gt;A simplified version looks something like this:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight javascript"&gt;&lt;code&gt;&lt;span class="kd"&gt;function&lt;/span&gt; &lt;span class="nf"&gt;checkBacklinkIndexation&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;sheet&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;SpreadsheetApp&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;getActiveSpreadsheet&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
    &lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;getSheetByName&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;Backlinks&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;

  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;apiKey&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;PropertiesService&lt;/span&gt;
    &lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;getScriptProperties&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
    &lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;getProperty&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;SERPAPI_KEY&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;

  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;rows&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;sheet&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;getDataRange&lt;/span&gt;&lt;span class="p"&gt;().&lt;/span&gt;&lt;span class="nf"&gt;getValues&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;

  &lt;span class="c1"&gt;// Assume column A contains backlink URLs&lt;/span&gt;
  &lt;span class="k"&gt;for &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="kd"&gt;let&lt;/span&gt; &lt;span class="nx"&gt;i&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="nx"&gt;i&lt;/span&gt; &lt;span class="o"&gt;&amp;lt;&lt;/span&gt; &lt;span class="nx"&gt;rows&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;length&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="nx"&gt;i&lt;/span&gt;&lt;span class="o"&gt;++&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;backlinkUrl&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;rows&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="nx"&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="k"&gt;if &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="o"&gt;!&lt;/span&gt;&lt;span class="nx"&gt;backlinkUrl&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
      &lt;span class="k"&gt;continue&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt;

    &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;query&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="s2"&gt;`"&lt;/span&gt;&lt;span class="p"&gt;${&lt;/span&gt;&lt;span class="nx"&gt;backlinkUrl&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;"`&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

    &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;endpoint&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt;
      &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;https://serpapi.com/search.json&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt; &lt;span class="o"&gt;+&lt;/span&gt;
      &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;?engine=google&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt; &lt;span class="o"&gt;+&lt;/span&gt;
      &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;&amp;amp;q=&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt; &lt;span class="o"&gt;+&lt;/span&gt; &lt;span class="nf"&gt;encodeURIComponent&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;query&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;+&lt;/span&gt;
      &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;&amp;amp;api_key=&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt; &lt;span class="o"&gt;+&lt;/span&gt; &lt;span class="nf"&gt;encodeURIComponent&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;apiKey&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="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;response&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;UrlFetchApp&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;fetch&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;endpoint&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="na"&gt;muteHttpExceptions&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kc"&gt;true&lt;/span&gt;
      &lt;span class="p"&gt;});&lt;/span&gt;

      &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;data&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;JSON&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;parse&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;response&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;getContentText&lt;/span&gt;&lt;span class="p"&gt;());&lt;/span&gt;

      &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;indexed&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;data&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;organic_results&lt;/span&gt;&lt;span class="p"&gt;?.&lt;/span&gt;&lt;span class="nf"&gt;some&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;result&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt;
        &lt;span class="nx"&gt;result&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;link&lt;/span&gt; &lt;span class="o"&gt;===&lt;/span&gt; &lt;span class="nx"&gt;backlinkUrl&lt;/span&gt;
      &lt;span class="p"&gt;);&lt;/span&gt;

      &lt;span class="nx"&gt;sheet&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;getRange&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;i&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="mi"&gt;2&lt;/span&gt;&lt;span class="p"&gt;).&lt;/span&gt;&lt;span class="nf"&gt;setValue&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
        &lt;span class="nx"&gt;indexed&lt;/span&gt; &lt;span class="p"&gt;?&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;Indexed&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;Not Indexed&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;
      &lt;span class="p"&gt;);&lt;/span&gt;

    &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="k"&gt;catch &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;error&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
      &lt;span class="nx"&gt;sheet&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;getRange&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;i&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="mi"&gt;2&lt;/span&gt;&lt;span class="p"&gt;).&lt;/span&gt;&lt;span class="nf"&gt;setValue&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;Error&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt;
  &lt;span class="p"&gt;}&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;This is intentionally simplified. In a real implementation, I wouldn't rely on an exact string comparison alone, since URLs can differ by trailing slashes, query parameters, redirects, encoding, or canonical versions. The important part is the data flow rather than this exact comparison: read the rows, skip empty URLs, build a search request, send it to SerpApi, parse the JSON response, determine whether the target appears in the returned results, and write the status back into the spreadsheet.&lt;/p&gt;

&lt;p&gt;I also keep the API key in Apps Script's script properties rather than hardcoding it into the source. That makes configuration easier to manage and avoids accidentally leaving the key inside a function that might get copied around.&lt;/p&gt;

&lt;h2&gt;
  
  
  Why I intentionally made it manual-only
&lt;/h2&gt;

&lt;p&gt;This was probably the most deliberate engineering decision in the project.&lt;/p&gt;

&lt;p&gt;Apps Script supports time-driven triggers, so technically I could make the checker run automatically every hour, every day, or on some other interval. I decided not to. The reason is API cost.&lt;/p&gt;

&lt;p&gt;Every backlink check results in an external API query, and SerpApi charges per search. An automatic trigger can quietly consume the available quota. Imagine a spreadsheet with 500 URLs — if an automated process checks all of them daily, that's potentially 15,000 searches a month, and the user might not notice until the quota is already gone.&lt;/p&gt;

&lt;p&gt;For this particular tool, automation isn't necessarily an improvement. So instead, I added a custom menu and the user explicitly starts the process:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Open spreadsheet
       |
       v
Review backlink list
       |
       v
Run "Check Indexation"
       |
       v
API requests are made
       |
       v
Results appear in the sheet
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;This makes API usage visible, and it gives me better control when testing. I can add a few URLs, run the checker, inspect the output, and stop there. The trade-off is obvious: manual execution is less convenient than a scheduled job. But here I preferred predictable API usage over invisible automation.&lt;/p&gt;

&lt;h2&gt;
  
  
  The technical problem I ran into
&lt;/h2&gt;

&lt;p&gt;One issue appeared once I started testing with larger backlink lists. The first implementation treated the spreadsheet almost like a normal JavaScript array and made an individual request for every row, followed immediately by an individual spreadsheet write. That works fine for a small test. It becomes inefficient with hundreds of URLs, because there are two different external operations happening repeatedly — a sheet read, an API request, a sheet write, over and over.&lt;/p&gt;

&lt;p&gt;Apps Script has execution time limits, and every network request adds latency. Writing to Sheets cell-by-cell also creates unnecessary overhead.&lt;/p&gt;

&lt;p&gt;The fix was to read the spreadsheet data once and collect results in memory instead of writing on every iteration:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight javascript"&gt;&lt;code&gt;&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;results&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="p"&gt;(&lt;/span&gt;&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;url&lt;/span&gt; &lt;span class="k"&gt;of&lt;/span&gt; &lt;span class="nx"&gt;urls&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;status&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;checkUrl&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;url&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
  &lt;span class="nx"&gt;results&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;push&lt;/span&gt;&lt;span class="p"&gt;([&lt;/span&gt;&lt;span class="nx"&gt;status&lt;/span&gt;&lt;span class="p"&gt;]);&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Then write the complete result range in a single operation:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight javascript"&gt;&lt;code&gt;&lt;span class="nx"&gt;sheet&lt;/span&gt;
  &lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;getRange&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;2&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;2&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;results&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;length&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
  &lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;setValues&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;results&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Small change, but it matters once the spreadsheet gets larger. I also learned that API response handling needs to be defensive — a successful HTTP request doesn't automatically mean the expected JSON structure exists. An API can return an error, a quota message, or a response missing the expected fields. So the production version checks the HTTP response and expected fields before deciding a URL is "Not Indexed." An API failure and a confirmed non-indexed URL are two completely different states, and conflating them silently would've made the data untrustworthy.&lt;/p&gt;

&lt;h2&gt;
  
  
  What I'd improve next
&lt;/h2&gt;

&lt;p&gt;A few things I'd change if I keep developing this.&lt;/p&gt;

&lt;p&gt;Batching. Large lists should be processed in controlled chunks rather than treating the entire spreadsheet as one job.&lt;/p&gt;

&lt;p&gt;Caching. If I checked a URL yesterday and it hasn't changed, there's little reason to spend another API query re-checking it. Storing the last-checked date and result would let me re-check only URLs that actually need another lookup.&lt;/p&gt;

&lt;p&gt;Better error reporting. Instead of just writing "Error," the sheet could store the actual HTTP status, a quota message, or a note on the malformed response — useful context instead of a dead end.&lt;/p&gt;

&lt;p&gt;A progress indicator. Since API requests take time, something like "42 / 200 checked" would make a manual run easier to follow.&lt;/p&gt;

&lt;p&gt;The basic version is already useful because it removes a repetitive lookup process from the browser and puts the result directly next to the backlink data it's describing. The bigger lesson for me was that a small internal tool doesn't always need a complicated architecture — Google Sheets, Apps Script, and one external API were enough. The more interesting engineering decisions were around API cost, execution limits, error states, and knowing when automation actually helps versus when it just hides a cost you'll notice later.&lt;/p&gt;

&lt;p&gt;Making the tool manual-only wasn't a limitation I forgot to solve. It was the feature that kept the system predictable.&lt;/p&gt;

</description>
      <category>googleappsscript</category>
      <category>javascript</category>
      <category>automation</category>
      <category>seo</category>
    </item>
  </channel>
</rss>
