Not "pending". Not "blocked". Crawled, fetched successfully, canonical accepted, and then declined. This post is the diagnostic path I took, the two measurement mistakes I made along the way, and the Search Console API calls that finally produced a straight answer.
The site is a Vite + React SPA prerendered at build time by Puppeteer. Every route ships as a static HTML file with the content already in the markup, served by nginx. Not SSR, but for a crawler it is indistinguishable: the body arrives full.
The obvious hypotheses, in the order everyone tries them: Google cannot render the JavaScript. robots.txt is blocking something. Canonicals point somewhere else. The pages are thin or duplicated. There is a manual action.
381 KB of HTML, one , correct canonical, index, follow. Rendering was never the problem.
For internal links, count real anchors rather than trusting a framework's router:
That matters because a React app that navigates with onClick handlers instead of is invisible to a crawler's link graph. This one was fine.
Everything above tells you what you serve. It says nothing about what Google decided. For that there are exactly two useful endpoints.
Run it over a representative sample rather than one URL. Mine came back like this:
Every one of them: robotsTxtState: ALLOWED, pageFetchState: SUCCESSFUL, lastCrawlTime within the last two weeks, googleCanonical matching userCanonical.
That combination is unambiguous. Google arrived, fetched a 200, parsed the page, agreed with my canonical, and chose not to index. There is no technical defect to fix, because nothing failed.
The aggregate view confirmed it: 2 indexed, 185 not indexed — 99 "Discovered, currently not indexed" and 86 "Crawled, currently not indexed".
Two measurement mistakes worth stealing quickratio will tell you your pages are duplicates when they are not
Template-generated pages (/compare/a-vs-b, /use-cases/industry) are the obvious suspects for thin content. So I measured similarity:
Damning, apparently. Except quickratio() compares character frequency multisets, not sequences. Two unrelated English documents of similar length score high on it by construction. It is a cheap upper bound designed to skip expensive comparisons, not a similarity metric.
Result: 90-95% unique per page, 1400-1950 words of it, with only ~35 words of shared boilerplate (nav, CTA strip, footer). The pages were not the problem. Had I stopped at quick_ratio I would have spent a week rewriting content that was already fine. site: in a search box is not an index count
num=100 no longer works on Google. If you count results on the first page you are counting ten. Paginate with start= and stop when a page returns fewer than a full set, or just read the Page Indexing report, which is authoritative.
Ten inbound links from three domains, all to the homepage. Meanwhile a backlink tool reported 197 links — but more than half came from a single PBN spam network hammering one keyword-stuffed anchor. Google had simply not counted them. The tool's number was noise; Search Console's was the signal.
So: a site with real content, clean markup, correct canonicals, valid sitemaps, no manual action, and effectively zero independent references, publishing 230 pages into one of the most saturated categories on the web.
