DEV Community

Kimusta
Kimusta

Posted on

Google Indexed 258 of My 406 Pages. The Sitemap Was Never the Problem.

I run Kimusta, a marketplace for local services in Turkey. 406 URLs in the sitemap, every one returning HTTP 200, every one with its own title, description and canonical.

Google indexed 258 of them and left 140 in Discovered — currently not indexed.

The obvious suspects were all clean: no crawl errors, no manual actions, robots.txt correct, sitemap read successfully. So I crawled all 406 URLs myself, with JavaScript disabled, and built a link graph from the raw HTML. The answer was embarrassing.

The homepage linked to four pages

Distance from / Pages
0 1
1 4
2 or more 0
Unreachable without JS or the sitemap 401

Four links. On a site with 406 pages.

They pointed at /hizmetler, /hizmet-verenler, /iletisim and /yasal — and all four were single-page app shells. Their HTML body was empty; the content arrived after the JavaScript bundle did.

The cause: navigation was mounted by JavaScript

Every server-rendered page started like this:

<header id="commonSiteHeaderMount" class="site-header"></header>
<main id="main"> ...real content... </main>
<footer id="commonSiteFooterMount" class="site-footer"></footer>
<script src="/js/common-site-header.js" defer></script>
<script src="/js/common-site-footer.js" defer></script>
Enter fullscreen mode Exit fullscreen mode

The content was server-rendered. The navigation was not. Menu and footer links — the most important internal links on any site — only existed after the defer scripts ran.

Total internal links across the whole site, as reported by Search Console: 970. That is 2.4 per page.

The catalogue hub had zero links

/hizmetler was the worst case. After the SPA rendered, the page had 323 words and 10 links — all of them menu items. None pointed at any of the 119 service pages it was supposed to list.

The 119 service pages, in turn, received a median of one internal link each.

What I changed

Three server-side changes. No framework, no build step.

1. Render the chrome on the server

One PHP partial returns exactly the markup the JavaScript was already producing:

// lib/site-chrome.php
function k_ust_chrome_header(): string {
    return <<<'HTML'
<header class="site-nav">
  <a class="brand" href="/">KIMUSTA</a>
  <nav class="desktop-nav">
    <a href="/hizmetler">Hizmetler</a>
    <a href="/hizmet-veren-basvurusu">Hizmet Veren Ol</a>
  </nav>
</header>
HTML;
}
Enter fullscreen mode Exit fullscreen mode

The templates print it straight into the mount:

<header id="commonSiteHeaderMount">
  <?= k_ust_chrome_header() ?>
</header>
Enter fullscreen mode Exit fullscreen mode

The client script stays. When it runs it writes the same markup into the same element, so there is no hydration mismatch — it only swaps the session-dependent buttons (login / account).

2. Give the hub a real body

I replaced the /hizmetler shell with a server-rendered A–Z index: 119 services, each linked, grouped by letter, with its parent category as a label. 46 KB of HTML that works with JavaScript switched off.

3. Measure the same thing again

Metric Before After
Links on a city page 17 38
Links on /hizmet 44 78
Links on /hizmetler 2 142

The other 140 pages are a different problem

Those 140 URLs are city pages: 20 categories × 12 cities, generated from one template. Measured across a category, the text overlap is 91%. Only the city name changes.

That is not a crawling bug. It is a content verdict, and no amount of internal linking fixes it.

What I did not do is delete them. They are linked, they return 200, and the catalogue is still growing — a category that has one service today will have ten next quarter, and its city pages become useful on their own. Deleting URLs to please a crawler is how you lose the pages you will want later.

The honest plan is slower: write content that actually differs per city (provider counts, price ranges, seasonal demand) and let the thin ones stay unindexed until they earn it.

Five things I took away

  1. Measure crawl depth, not sitemap size. A 406-URL sitemap tells you nothing about what Google can reach by following links.
  2. If a component is navigation, render it on the server. Hydration is for interactivity, not for structure.
  3. Never ship an empty mount for anything you want indexed. An SPA shell returns 200 and looks fine in a browser, which is exactly why it survives review.
  4. Discovered — currently not indexed is a quality signal. 140 pages said it at once; that was the content model talking, not a technical fault.
  5. Programmatic pages are cheap to generate and expensive to earn. Generating them took an afternoon. Everything since has been the real work.

The site is at kimusta.com; the catalogue index that replaced the empty shell is at kimusta.com/hizmetler.

Top comments (2)

Collapse
 
sungprofile profile image
Ruri Jupjup (루리의 줍줍) •

The link-graph crawl with JS off is such a good diagnostic, and "4 links from the homepage on a 406-page site" is a number I'll remember. We hit a cousin of this on a static info site: navigation was fine, but most detail pages were only reachable from paginated lists, so they sat at depth 4-6. What helped most was adding hub pages grouped by topic and region (each linking to the 10-30 detail pages under it) and linking those hubs from the footer. Did you see the "Discovered - not indexed" count move once the header/footer links were server-rendered, and how long did it take?

Collapse
 
mercerk17 profile image
Kai Mercer •

Building the link graph from raw HTML with JavaScript off is the right way to find this, and the "unreachable without JS or the sitemap" row makes the point better than any advice would.

One check to add after the fix: re-crawl the same way and track the number of pages at distance 3 or more from the homepage, not just whether each page has a link. A page reachable only through a long chain of category pages can still get crawled late. Did you see the "Discovered" count start to fall after the change, and how long did that take?