The GA4 dashboard showed 2 events per seven days. The site had over seventy articles.
The first assumption is always tracking. Broken GTM container, misconfigured event filter, wrong property ID. I ran through the list: GTM preview showed events firing, property ID matched, internal traffic excluded. Tracking was fine. The number was real.
That shifts the question. Not “why isn’t GA4 recording this?” but “why isn’t there anything to record?”
What GSC showed
Google Search Console over the prior 28 days: 5 impressions, 1 click.
Not per article. Total.
A site with 70+ articles, a working deploy pipeline, content published over months, showing up in Google search results five times in 28 days. That’s not underperformance. That’s absence.
The measurement was accurate. The site was simply not being indexed.
The audit
I ran an 8-dimension ultracode audit against the site: 161 agents, roughly 7.7M tokens. Four parallel diagnostic agents, one synthesis pass. The pattern was clear: a discovery problem dressed as a measurement problem. That distinction matters more than it sounds. The debugging path for a measurement problem leads to GTM and Analytics configuration. The path for a discovery problem leads to crawlability, indexing, and canonicalisation.
I identified the primary cause: canonical tags deindexing the pages they were meant to represent.
What canonical deindexing does
A canonical tag tells Google: “this URL is a copy; the real version is at [other URL].” Google obeys. It indexes the nominated canonical and ignores the page carrying the tag.
Used correctly, this collapses duplicate content onto a single URL and consolidates link equity. Used incorrectly, it instructs Google to ignore the page you intended to rank. Incorrect means pointing at a non-existent URL, nominating a www variant when the server canonicalises apex, or routing through a redirect chain. The content exists. Google follows the instruction and skips it.
Eight pages on the site were in this state. From Google’s perspective, those articles didn’t exist.
The www→apex redirect
The secondary cause was a missing 301 from www to apex. www.captainrandom.co.uk and captainrandom.co.uk are different origins to a search engine. When canonical tags nominate the apex domain as authoritative but the server responds on both, Google sees two URL spaces. Link equity splits between them. Indexing signals split. Neither accumulates enough authority to rank.
The fix: a blanket 301 at the infrastructure layer. Any request to the www subdomain returns a permanent redirect to apex before content is served. One canonical namespace.
Unblocking 8 pages
The canonical tags on the 8 affected pages pointed at URLs that either redirected elsewhere or didn’t resolve. The source of the error was per-page configuration rather than a single function. Each page had been generating its canonical independently, and some had been set incorrectly.
The fix consolidated canonical URL generation to a single function in the Next.js metadata layer. All article routes now derive their canonical from one place. The 8 pages now self-canonicalise to the correct apex-domain URL.
Homepage post density: 3 → 9
The homepage’s writing rail surfaced 3 articles. Google’s crawler follows links. If the highest-authority URL on the domain links to 3 articles, the crawler has 3 paths into the content. Raise that to 9 and you add 6 crawl entry points from the surface that carries the most PageRank.
The change was a hardcoded constant. 3 → 9. The writing rail already handled N items; nothing else needed to change.
RelatedPosts
I flagged internal linking as a gap during the audit. Articles were islands. A reader finishing one article had no obvious path to a related one. Google’s crawler had the same constraint: after indexing an article, no onward links into the rest of the content graph.
The fix was a RelatedPosts component on every article page, wired into writing/[slug]/page.tsx. Build-time computation in src/lib/related.ts derives relevance via IDF-weighted shared-tag overlap across the full article corpus, so tags appearing on many articles score lower while tags shared by a smaller subset score higher. The result is a ranked list of genuinely related articles rather than a random selection from the same broad category. No API call at render time. Computed once per build, serialised to static props.
SERP-fit titles
PR #191 introduced TITLE_SUFFIX_BUDGET. It accounts for the “· Captain Random” suffix the layout appends to every title and trims the base title to keep the combined string under Google’s ~60-character SERP cutoff.
PR #196, which shipped the /learning/ hub, didn’t carry the logic to the new dynamic routes. Chapter titles in the learning hub overflowed the cutoff. Google truncates at ~60 characters. The title that appears in search results is not the title you wrote.
A follow-up patch applied TITLE_SUFFIX_BUDGET to every dynamic route on the site, including the topic and chapter routes under /learning/.
The educational hub
Topical authority is read partly through internal link structure. Does this site have a cluster of related pages linking to and from a central hub? Without that structure, individual articles compete on terms with no topical signal behind them.
/learning/ is the answer to that. A top-level surface with topic-level landing pages and chapter-level articles underneath. The first cluster covers GSC + GA4 instrumentation across four chapters. The hub page links to every chapter; each chapter links back to the hub and laterally to adjacent chapters.
That’s the link structure Google needs to read topical authority. I flagged it under two separate findings during the audit: no inline internal linking and no topical clusters. Both close with the same surface.
The asymmetry
Two weeks before the audit I was debugging tracking infrastructure. GA4 showing near-zero pointed at collection. That was the wrong layer.
Discovery collapse and measurement failure are indistinguishable from GA4 alone. Both show as missing data. The difference is which part of the stack produced the silence. Measurement failure: data exists, collection is broken. Discovery collapse: no data exists because no one found the pages to generate it.
GSC is the tell. GA4 measures what happens after someone arrives. GSC measures whether Google found the page at all. When both show near-zero independently, the problem is upstream of both. It’s in indexing.
The canonical deindex was upstream of everything. The site had been issuing Google explicit instructions to ignore its own articles. Fix the instructions; the measurement problem dissolves on its own.
What isn’t fixed yet
The 8 unblocked pages are now crawlable. They are not yet ranking. Getting into the index is necessary but not sufficient. Ranking runs on signals accumulated over weeks: link equity, click-through rate, dwell time, topical authority. The /learning/ hub and RelatedPosts component are building those signals going forward.
GA4 traffic won’t change this week. GSC impression counts have a crawl lag of four to eight weeks depending on Googlebot’s recrawl schedule. The effect will show; the timeline is months, not days.
Out of scope: the remaining content gaps I surfaced during the audit. Each is a separate sprint.



