Technical SEO · 2025 · 2 weeks
CanCon Radio
Rebuilding search indexing for a 39,600-page catalogue
A bilingual Canadian music discovery platform with 39,645 pages was nearly invisible to Google: 1,047 indexed pages, single-digit daily impressions. Audit-first diagnosis, root-cause fixes across canonicals, hreflang and sitemaps — and a verification gate so it can’t regress.

39,645
Pages carrying exactly one canonical tag
481 → 0
Dead URLs advertised in the sitemap
391 → 0
Duplicate sitemap entries
7
Post-build verification checks, zero failures
The client
CanCon Radio is a bilingual Canadian music discovery platform: an all-Canadian radio player, a directory of 39,000+ Canadian artists, chart archives spanning five decades, and an editorial blog — every page available in English and French.
The problem
Google Search Console was reporting the classic trio of indexing failures: "Duplicate without user-selected canonical," hundreds of 404s, and redirect errors. Despite ~20,000 known pages, Google had indexed only 1,047 — and daily impressions were in single digits. A site with a catalogue this deep should be a search powerhouse. It was nearly invisible.
The audit
Before changing anything, we crawled the entire sitemap, sampled hundreds of live pages, and tested every URL variant an indexer could encounter (http/https, www/apex, trailing slashes, URL parameters, pagination).
The 481 dead URLs were a data story, not a link-rot story: a chart archive had been legitimately trimmed years earlier, but the sitemap generator was still advertising every historical URL to Google. Root cause over patch.
| Finding | Scale |
|---|---|
| Pages with no canonical tag (homepage, charts, directory, all app pages) | ~52% of sampled pages |
| Pages declaring hreflang — on a bilingual site | 0 |
| Dead URLs submitted in the sitemap (Google’s exact complaint) | 481 |
| Duplicate URLs in the sitemap (slug collisions, index.html variants) | 391 |
| Internal links splitting ranking between two copies of every artist page | 5 link sources |
| French article fragments served as raw, unstyled thin content | 282 files |
| Sitemap pointing at URLs that never existed (*-None.html) | Systemic builder bug |
What we shipped
Canonical architecture, everywhere. Every page on the site — 39,645 of them — now carries exactly one absolute, self-referencing canonical tag pointing at the preferred https:// apex form. App-state query parameters never leak into canonicals, and the internal linking was consolidated so each artist has one canonical URL instead of two competing copies.
An honest hreflang strategy. The site renders French client-side — there were no French URLs to point at. Rather than fabricate alternates, we made the language genuinely URL-addressable (?lang=fr, deep-linkable, wired to the existing translation layer and French article bodies), then declared the full en / fr / x-default trio on pages that have real French renderings — and only en + x-default on English-only pages.
Sitemaps rebuilt for scale. One 40k-line sitemap became an index of sectioned files (pages / artists / charts), deduplicated, canonical-URL-only, with lastmod on every entry and automatic sharding as the catalogue grows. robots.txt now advertises the index, and the builder emits correct URLs by construction — the *-None.html bug can’t recur.
Redirects with one hop. Host-level variants (http://, www.) were verified as single-hop 301s; legacy URLs that changed historically now land via permanent-redirect stubs with canonical targets.
A verification gate, so it can’t regress. A post-build checker validates all 39,645 pages against seven rules — canonical present/absolute/self, hreflang complete per page type, sitemap presence and reachability, no dead internal links, no duplicate canonical claims. The deployed site passes with zero failures.
Under the hood: the incident worth writing up
Mid-project, diagnostics revealed that a week of data-pipeline work had been writing to deleted files: the deployment automation’s git cycles were replacing files underneath long-running processes, stranding their output on invisible inodes (confirmed by inode forensics). We recovered the lost work, untangled the outputs from version control, switched harvesters to inode-immune write patterns, and made API-throttled jobs record skips instead of false negatives — so a rate-limited request can never silently become a permanent "no match." Rigorous recovery, no lost data, better system afterward.
Results
Post-fix indexing is intentionally measured in weeks, not days — Google’s recrawl of 39,000 pages is gated by the site’s own crawl-delay. The structural signals Google uses to decide what to index and why are now unambiguous: one canonical form per page, honest language relationships, and a sitemap that contains only URLs that exist.
| Metric | Before | After |
|---|---|---|
| Pages with canonical tags | ~52% of sample | 100% (39,645/39,645) |
| hreflang coverage | 0 pages | Every page, per language policy |
| Dead URLs in sitemap | 481 | 0 |
| Duplicate sitemap entries | 391 | 0 |
| Verification gate | didn't exist | 7 checks, 0 failures |
| Pre-fix GSC baseline | 1,047 indexed, ~0 impressions | Re-submitted; indexing in progress |
What this demonstrates
- ✦Audit-first methodology — crawl, sample, and protocol-matrix before touching code.
- ✦Root-cause discipline — the 404 wave traced to a data change, not patched with redirects.
- ✦Constraint-driven engineering — correct SEO on a serverless static host with no .htaccess, no server rendering, and client-side i18n.
- ✦Verification as a deliverable — the rules are enforced by the build, not by memory.
Stack
Outcome
The structural signals Google uses to decide what to index are now unambiguous across all 39,645 pages — one canonical form per page, honest language relationships, and a sitemap that contains only URLs that exist. Enforcement lives in the build, not in memory.