Search Console has a status that reads like a contradiction: Discovered, currently not indexed. Google knows the URL exists. Google has chosen not to spend a crawl on it.
CogniPrep had 28 pages sitting in that bucket: 12 provider tips posts, 15 pages in the "can you cheat this test" cluster, and one test-format page. All 28 were in the sitemap, returned 200, had unique titles and descriptions, and were reachable from their own index page. The sitemap had done its whole job. Discovery was never the problem.
What that status actually means
It is a budget decision. Being in a sitemap tells Google a URL exists; it does not tell Google the URL matters. The signal that says "this matters" is an internal link from a page Google already thinks is worth crawling, and the 28 pages had exactly none.
They were not strictly orphaned. /blogs links every post and /cheating links every guide. But an index page that itself gets very little search traffic is a weak endorsement. Nothing that already ranked pointed at them, and on this site the pages that already rank are obvious: the provider hubs. One of them pulled 18,632 impressions and 1,491 clicks in three months at an average position of 8.1. The tips post about that same provider, three clicks deep from anywhere Google cared about, had never been fetched.
So the fix is not a technical SEO change. It is: put the links where the attention is.
The component is a join, not a list
The obvious implementation is a hand-written map of provider to guide slugs. It would work on the day it was written, and rot from the first guide anybody added.
What shipped instead derives the relationship from data that already exists. A blog post declares which games it is about. A game declares which provider it belongs to. The provider hub joins them:
export function ProviderGuides({ provider, heading, intro }: ProviderGuidesProps) {
const posts = BLOG_POSTS.filter((post) =>
post.relatedGames?.some((id) => getGameById(id)?.provider === provider)
)
.sort((a, b) => b.date.localeCompare(a.date))
.slice(0, 5);
const cheating = getCheatingGuide(provider);
if (!cheating && posts.length === 0) return null;
// ...
}
Three deliberate choices in nine lines.
The post does not store a provider. It stores game ids, and the provider comes from the game library. A provider field on the post would be a second copy of a fact the library already owns, and the two copies would eventually disagree. Going through getGameById also means a post about three games from two providers correctly appears on both hubs, with no extra field and no special case.
The cap is a layout budget, not an SEO one. Five posts, newest first. One hub has twelve matching posts and shows five of them. More links on the page is better for crawling and worse for reading, and the section is on the page a candidate actually uses, so reading wins. The other seven are not lost; they are still on /blogs, and they are now one hop from a page that ranks instead of zero hops from one that does not.
The empty case returns null. A section heading with nothing under it is worse than no section, and it would ship the moment someone added a provider before writing anything about it.
Across 24 hubs that renders 91 links: one "can you cheat it" guide per provider, plus 67 post links. Not one of them is typed into a page.
Why the component sits on the hub and not in a footer
A site-wide "related reading" footer would spray the same links onto every page and get ignored by readers and discounted by crawlers, which is roughly what a link farm looks like from the outside.
The hub version passes a test the footer version fails: a person on /games/shl about to practise an SHL test is the exact person who wants to read what SHL tests reward and why shortcuts do not work. The intro copy says so out loud:
Read up before you practise: what the tests reward, what catches candidates out, and why shortcuts do not work.
If the only defence of an internal linking block is that crawlers like links, it is the wrong block. This one earns its place with readers first, and the crawl benefit is a consequence.
The comment above the component records why it exists, which is the sort of thing that gets lost in six months:
/**
* Exists because Search Console showed the provider tips posts and the whole
* /cheating cluster sitting in "Discovered, currently not indexed" for weeks.
* Nothing that already ranked linked to them, so Google never spent a crawl on
* them. The hubs are the pages with the most impressions on the site, which
* makes them the right place for those links.
*/
Measuring it honestly
The change shipped with a baseline table, not a prediction: live Google position for every provider hub on its head term, plus three months of Search Console impressions, clicks and average position per hub, captured the same day. Re-run in four to six weeks and compare. Anything else is a story.
That matters here because internal linking has no deploy-time feedback. Nothing in CI can tell you a page got indexed. The only honest check is the same numbers, later.
See it: open cogniprep.app/games/shl and find "Guides and Tips". Six cards: one /cheating/shl link and five /blogs/... links, newest first. Now open cogniprep.app/games/ixly and look at the same section: two cards, because that is everything the registries know about that provider. Same component, no per-provider configuration, and the difference between the two pages is entirely data.
For the other side of the join, cogniprep.app/blogs lists the posts themselves. Any post you see on a hub is there, and the hub decided to show it by reading the games it covers.
Top comments (0)