Illustrative case study

A fictional business. The figures are a modelled scenario, not a client result.

Ferrovane · Observability & logging platform · Berlin

How an observability platform made 6,000 JavaScript-rendered docs pages visible to Google

+341%indexed docs pages
in 8 months · Signal
6,040 docs URLsM0 · 1,120M8 · 4,940
CH1 · INDEXED DOCS
SWEEP = 8 MONTHS
TRIG · ARMED

Input signal · the challenge

Six thousand pages of docs, and Google could read almost none of them.

Ferrovane's documentation was a client-side SPA, so Google indexed less than a fifth of it and developers landed on competitors' guides instead.

Ferrovane’s docs were its best acquisition channel on paper: 6,040 guides, collector recipes and API references written by the engineers who built the product. But the site was a client-rendered single-page app. Googlebot fetched an empty shell, queued it for rendering, and often gave up before the content loaded. Developers searching for exact error messages and config snippets found competitors’ tutorials, many of them explaining how to wire up Ferrovane.

18.5%of docs URLs indexed at the start (1,120 of 6,040).
What Googlebot fetched
<html>
  <body>
    <div id="root"></div>
    <script src="/app.9f3c.js">
    <!-- 1.9 MB -->
  </body>
</html>
What a developer saw
# Configure the OTel collector

exporters:
  ferrovane:
    endpoint: ingest.eu…
    compression: zstd

Diagnosis · fault log

Seven faults between the docs and the index.

We read 30 days of server logs, compared raw and rendered HTML for every URL, and crawled the site with JavaScript off. The faults fell into four bands: rendering, architecture, crawl budget and international.

  1. E-101CRITRendering

    Docs shipped as an empty <div id="root"> plus a 1.9 MB bundle. Googlebot's render queue reached most pages days late, and timed out on API-reference pages that fetched content after hydration.

    > Raw-HTML word count: 0 on 5,880 of 6,040 URLs

  2. E-102CRITRendering

    Navigation was built from onClick handlers on <span> elements, so the crawler found no <a href> links between sections. Most guides were orphans.

    > Internal links in raw HTML: 3 per page on average

  3. E-207MAJArchitecture

    Six release versions (v3.0 to v3.5) published under different paths with no canonicals. Google kept choosing old v3.1 pages as the representative URL.

    > Duplicate clusters: 2,310 URLs across 6 versions

  4. E-214MAJArchitecture

    Hash-fragment routing (/docs#/collectors/otel) collapsed thousands of pages into one URL as far as search engines could tell.

    > Unique crawlable URLs: 412 of 6,040

  5. W-318MAJCrawl budget

    Faceted API-explorer parameters and session tokens created an effectively infinite URL space. Googlebot spent 71% of its requests on it.

    > Crawl spent on non-canonical URLs: 71%

  6. W-322MINCrawl budget

    Soft-404s on renamed pages returned 200 with a 'not found' component. Sitemaps listed 1,400 URLs that redirected.

    > Crawl errors logged per week: 3,840

  7. W-405MAJHreflang

    German and Japanese docs were injected client-side on the same URL as English, so there was no locale URL to annotate and the DE pages were never indexed.

    > Indexed DE/JA docs pages: 0

Calibration · the strategy

Four channels, each tuned separately.

The work ran on our Signal plan for technical SEO. Each tactic fixed one source of noise, and each came with a check so the fix could not quietly regress on the next release.

CH1

JavaScript rendering

Moved the docs from a client-only SPA to static pre-rendering at build time, with incremental regeneration on each release. Every page now ships its full text, headings and code samples in the first HTML response; JavaScript only adds interactivity.

Raw-HTML parity check run in CI: the build fails if rendered and raw text differ by more than 2%.

CH2

Docs architecture

Replaced hash routes with real paths (/docs/collectors/otel), turned span navigation into anchor links, and introduced a 'latest' canonical with versioned pages pointing at it. Breadcrumbs and a 3-click maximum depth to any guide.

Orphan pages: 4,100 → 0. Median click depth: 7 → 3.

CH3

Crawl budget

Blocked explorer facets and session parameters in robots.txt, stripped tokens from links, returned real 404/410 status codes, and split sitemaps by section with accurate lastmod dates fed from the docs repo.

Share of crawl on canonical docs URLs: 29% → 94%.

CH4

Hreflang

Gave each locale its own subfolder (/de/, /ja/) and server-rendered it, then added reciprocal hreflang and x-default in both the HTML head and the sitemaps, generated from the translation manifest so they cannot drift.

Hreflang return-tag errors: 1,960 → 0.

Time base · 1 div = 1 month

8 months, probe to lock.

  1. M01 · PROBE

    Crawl and render audit

    Log-file analysis of 30 days of Googlebot hits, raw vs rendered HTML diff on every URL, and a baseline of 1,120 indexed docs pages.

  2. M02 · PROBE

    Pre-render pilot

    Collector guides (420 pages) pre-rendered behind a flag. Indexed count in that section rose from 61 to 388 in five weeks.

  3. M03 · CALIBRATE

    Rendering rollout

    Static rendering shipped for all 6,040 pages. CI parity check added. Span navigation rebuilt as real links.

  4. M04 · CALIBRATE

    URL migration

    Hash routes replaced with paths, 6,000+ one-to-one 301s, version canonicals pointed at 'latest'.

  5. M05 · FILTER

    Crawl budget clean-up

    Facet and token parameters blocked, soft-404s fixed, sitemaps split by section with true lastmod.

  6. M06 · FILTER

    Locale subfolders

    German and Japanese docs moved to /de/ and /ja/, server-rendered, with reciprocal hreflang.

  7. M07 · LOCK

    Content gaps

    Comparison and how-to pages written for 140 developer queries where competitors' guides ranked and Ferrovane had no page.

  8. M08 · LOCK

    Monitoring and hand-off

    Render, index and hreflang checks wired into the release pipeline, with alerts to the docs team.

Readout · results

Tune each channel. The signal was there all along.

  • Indexed docs pages
    1,120 → 4,940
  • Organic sign-ups
    +88%
  • Crawl errors
    -93%
  • Developer-query top-10s
    +620

Modelled scenario for a fictional business — not a client result. Rankings and traffic are never guaranteed; see our Terms.

0/4 channels locked

CH1 · pages in indexNoise

Indexed docs pages

1,120 → 4,940

CH2 · organic sign-ups, index = 100Noise

Organic sign-ups

+88%

CH3 · crawl errors / weekNoise

Crawl errors

-93%

CH4 · new developer-query top-10sNoise

Developer-query top-10s

+620

Turn a knob (drag, or focus it and use the arrow keys) to pull each metric out of the noise.

Developer queries, before and after

Google position, modelled. “—” means not in the top 100.

QueryLocVol/moM0M8
opentelemetry collector configEN6.6k48#3
structured logging kubernetesEN2.9k—#6
fluent bit json parserEN3.4k22#4
trace sampling strategiesEN1.6k—#8
promql vs logqlEN1.3k67#5
log retention policy gdprEN88031#2
log management dsgvoDE1.1k—#1
opentelemetry exporter einrichtenDE590—#3
ログ 監視 ツール 比較JA1.9k—#7

One results page, before and after

“opentelemetry collector config”, modelled.

M0 · #48

  1. 1. Competitor tutorial: OTel collector setup
  2. 2. Community wiki: collector config reference
  3. 3. Blog: shipping logs with the collector

… Ferrovane on page 5

M8 · #3

  1. 1. OpenTelemetry project docs
  2. 2. Competitor tutorial: OTel collector setup
  3. 3. Ferrovane Docs › Collectors › Configure the OTel collector

End of trace

Get a plan like this

If search engines can’t see what your users see, Signal starts with a render and crawl audit and turns the findings into fixes your engineers can ship.