we broke our own seo: 24 pages returning 200 and saying not found
in august 2026 we finally pointed our own audit process at nanushi.org. we had been putting it off for the reason everyone puts it off — the site looked fine. it loaded fast, it was linked from client sites, it had sixty-odd blog posts on it. the only thing wrong was that in sixteen months it had received zero impressions in google search. not few. zero.
the comfortable explanation was sitting right there: the content is not competitive enough. that explanation is unfalsifiable, it requires no work, and it turned out to be wrong.
this is the whole diagnosis, in the order it actually happened, including the part where we had been misreading our own tooling.
the first number was a lie
we connected a service account to the search console api and ran a report. it came back empty. no clicks, no impressions, no data at all — which was consistent with everything we already believed.
it was also wrong. the property in search console is a domain property, which the api addresses as sc-domain:nanushi.org. we had configured it as a url, https://nanushi.org/. give a domain property a url and the api does not throw an error. it returns an empty result set.
an empty result set from a misconfigured property and an empty result set from a site nobody visits look exactly the same. this cost real time, and it is the reason the first step of our audit process is now "confirm the tool can see the property at all" rather than "read the numbers".
with the property string fixed, the picture changed:
properties visible: 1
sc-domain:nanushi.org (siteFullUser)
last 28 days : clicks=0 impressions=0
last 16 mths : clicks=0 impressions=0
homepage : submitted and indexed, crawled same day
everything else : discovered - not indexed, or unknown. crawled=never
sitemaps : none ever submitted
still zero impressions, but now with a cause attached. google had crawled exactly one page of the site. the homepage. everything else it either knew about and had declined to fetch, or had never heard of.
that distinction matters more than any other number in the report. indexed with no impressions is a content problem. not indexed is a crawl problem. the first is slow and uncertain to fix. the second is usually a bug, and bugs get fixed. we had the second one. more on reading these states in how to actually read google search console.
a cluster of identical counts is one bug, not four
next we crawled the site ourselves. the crawl reported four separate problems:
- 21 pages with duplicate titles
- 22 pages with duplicate meta descriptions
- 21 pages with a canonical tag outside the
<head> - 22 pages with multiple h1 tags
four findings, four tickets, four afternoons. except look at the counts. 21, 22, 21, 22. when a crawl reports several rules failing on nearly the same number of urls, the odds are overwhelming that you are looking at one broken template reported four ways. we now treat matching counts as a single lead rather than a backlog.
the step everyone skips
the check that found it is embarrassingly simple: take every url the sitemap advertises, and fetch it.
curl -s https://www.nanushi.org/sitemap.xml \ | grep -o '<loc>[^<]*</loc>' | sed 's/<[^>]*>//g'
103 urls came back. we fetched a sample. twenty-four of them returned http 200 — a success — and rendered a page that said "not found".
that is a soft 404, and it is worse than a real 404. a real 404 tells google plainly that a url is dead and google removes it. a soft 404 tells google the request succeeded, so google has to infer from the content that the page is worthless. a sitemap made up of twenty-four of them is a document that advertises your site as broken, submitted by you, on purpose, as far as google can tell.
the actual bug, in one line
we host a react native tutorial. its chapters live on disk as markdown files:
content/tutorials/react-native-fundamentals/chapters/
01-introduction.md
02-environment-setup.md
...
the route keys chapters without the ordering prefix. /tutorials/react-native-fundamentals/introduction renders. that prefix exists purely so the files sort correctly in a directory listing.
the sitemap generator read the same directory and emitted the filename:
url: `${baseUrl}/tutorials/react-native-fundamentals/${fileName.replace(/\.md$/, "")}`
so the sitemap published /01-introduction, and the router had never heard of it. twenty-four chapters, twenty-four soft 404s. and because every one of them fell through to the same not-found rendering, they all shared a title, all shared a description, all rendered the same two h1s, and all placed a canonical outside the head. there were the four clusters. one bug.
the fix is four characters of regex:
url: `${baseUrl}/tutorials/react-native-fundamentals/${fileName .replace(/\.md$/, "") .replace(/^\d+-/, "")}`
this class of bug is not rare and it is not a next.js problem specifically — it is what happens whenever the sitemap derives slugs from one source and the router derives them from another. any time those two are computed separately, they will eventually disagree. we go into the app router specifics in technical seo for next.js app router sites.
the second decision: do not 404 them
the instinct after fixing the generator is to let the bad urls 404. they were never real pages, after all.
that instinct is wrong here, and the reasoning is worth internalising because it comes up in every migration. those urls had been published in a sitemap for months. google had discovered them. a discovered url carries a small amount of signal, and a 404 throws that signal away, whereas a permanent redirect passes it to the page that should have had it all along.
so /01-introduction now 308s to /introduction. the rule we settled on:
- redirect a bad url that was published and discovered
- 404 a url that never existed anywhere
- never serve the same content at both addresses — that trades a soft 404 for a duplicate, which is a lateral move
the same logic drives redirect mapping in a redesign, which we cover in how to redesign your website without losing your seo rankings.
everything else the audit turned up
with the headline bug found, the rest of the list was quick:
the sitemap had never been submitted. not once, in the property's lifetime. we had been serving a perfectly valid /sitemap.xml for over a year, which is not the same thing as telling search console it exists. a lot of sites are in this state.
/ottawa was not in the sitemap at all. our local landing page — the single page most likely to earn commercial traffic for an ottawa business — was absent from the static list, and search console reported it as "url is unknown to google". see local seo for ottawa small businesses for why that page matters.
the homepage declared no canonical. every other page did. /community did not either. a page with no canonical lets google choose one, and google chose reasonably here, but relying on that is not a plan.
og:image was missing on all 100 pages. every share of every url rendered as a bare link. in next.js an app/opengraph-image.tsx gives every route one without anyone exporting a png.
our json-ld pointed logo and image at /logo.png, which 404'd. structured data asserting an asset that does not exist is worse than omitting the field — you have told google something checkable and it checks.
/blog and /faq had ~14 character titles in a slot that renders about 60.
what we ruled out, and why that is the point
we checked and dismissed: page speed (400–600ms loads, ttfb under 260ms), internal linking (the homepage links every main page), backlinks (four of five client sites link back), mobile rendering, robots.txt, and domain age.
none of it was the problem. that is the argument for running the checks in a fixed order with the cheap decisive ones first. if we had started with "the content is not competitive" we would have written twenty more blog posts into a sitemap that was advertising twenty-four broken urls.
the honest ending
we are writing this the day after the fixes went in and the honest answer is that we do not yet know whether they worked. the plan is to re-read search console around the third of september — seven to ten days out and look for one thing: pages moving from discovered - not indexed to indexed. if they move, the soft-404 cluster was the cause. if they do not, it is a quality judgement and needs a completely different answer.
we are deliberately not stacking further technical changes before that reading. change five things at once and you learn nothing about any of them.
and there is a finding we cannot fix with configuration, which we may as well publish too: our /missions page is 64 words. /contact is 131. /about is 288. /services is 326. those are the pages that would earn commercial traffic, and no amount of sitemap correctness rescues a 64-word page. that part is writing, not engineering.
if your site is not being found and you are not sure whether the cause is technical or editorial, that is precisely the question an audit answers first. see what our seo work covers, or read the plain-english groundwork in technical seo basics every website owner should understand.