GROX
Definitions

Why doesn't my single-page app show up in search?

Published 27 September 2026

A single-page app often serves the same index.html for every address. Search engines then see one title, one canonical and little or no text. Crawlers that never run JavaScript fetch an empty shell. Prerendering each route with its own title, canonical and readable copy is the usual fix. A static site is sometimes simpler.

Why does one index.html hide every route from search?

Most single-page apps put the router in the browser. The server answers every path with the same HTML file. That file names one document: one title, one description, one canonical URL, often pointing at the homepage. When a crawler asks for /pricing or /docs, it still receives that homepage document. Search systems then fold those addresses into the home listing because they look like duplicates of the same page.

The app may look complete after JavaScript runs. Ranking still starts from the first HTML response. If that response is identical for every path, extra routes do not become extra documents. Linking to them from the rest of the web does not help if the fetched document never changes.

What do crawlers actually fetch if they skip JavaScript?

Not every crawler executes scripts. Many fetch the first HTML, parse links and text, and stop. A typical SPA shell is a nearly empty body: a root element, a script tag, perhaps a loading spinner. There is no heading, no paragraph, no product name. The crawler records an empty page and may never return to run the bundle.

Even crawlers that do run JavaScript may time out, block third-party scripts, or index the shell first and treat later content as a weak update. You cannot assume they will wait for your router. The safe document is HTML that already contains the title, canonical and main text for that address.

What a crawler typically receives for each address
ApproachHTML per routeTitle and canonicalMain text without JavaScript
Client-rendered SPASame index.htmlUsually the homepage pairEmpty or a spinner
Prerendered SPAA snapshot per pathSet per pathPresent in the snapshot
Multi-page or static siteA file per pathSet per pathPresent in the file
Server-rendered appGenerated per requestSet per requestPresent in the response

How does prerendering each route fix titles and canonicals?

Prerendering means you visit each public path at build time (or on a schedule), let the app render, and save the resulting HTML. That file is what the server returns for that path. The title tag, the canonical link, Open Graph tags and the visible copy all belong to that route. A crawler that never runs JavaScript still reads a complete page.

Keep the client app for later interaction if you need it. The first paint and the first crawl should not depend on it. Generate a unique canonical for each indexable URL and point it at itself, not at the home path. Duplicate titles across routes are a signal that the snapshots were not split. After deploy, fetch the HTML with a tool that does not execute scripts and read the tags yourself.

Empty shell
The first HTML for a path contains almost no text, only a mount point and scripts.
Canonical
The link tag that names the preferred URL for this document. If every path names the home URL, other paths collapse into it.
Prerender
Saving rendered HTML for each public path so the server can return a full document without running the app in the crawler.
Indexable route
A public address you want listed as its own result, with its own title, canonical and readable copy.

How can you check that each address has its own page?

Request each important URL with a plain HTTP client. Confirm the status is success, the title matches that page, the canonical equals that URL, and a substantial block of text is present before any script. Compare two routes: if the HTML is byte-for-byte the same, search will treat them as one document.

Use the search engine’s URL inspection tools where you have them, but do not skip the script-free fetch. Sitemaps should list the real paths, not only the origin. If you ship with GROX Code, still fetch the live HTML the same way: the build pipeline does not change what a crawler sees unless each route is written out. A third request for the same missing heading should make you look at the server mapping, not patch the client again.

  • Fetch without JavaScript and read title, canonical and body text.
  • Compare HTML for two routes; identical files mean one document.
  • List real paths in the sitemap, not only the homepage.
  • Fix the server so each path returns its snapshot, then re-fetch.

When is a simpler static site the better choice?

If the public site is mostly articles, pricing and docs, a static generator or a multi-page server is often enough. You avoid teaching a crawler to wait for a router. You still get unique titles and canonicals because each file is a document. A single-page app is a better fit when the product itself is an application after login, and the marketing pages can live beside it as ordinary HTML.

Do not prerender private dashboards. Do not invent numbers about traffic recovery. The mechanism is simple: each indexable address must return its own HTML. If that is hard in your stack, split the public pages out. British teams often keep the app at app.example.com and the crawlable site on the apex; that split is a trade-off of two hostnames against one empty shell.

Common questions

Why does Google only show my homepage for a React app?

The server likely returns the same index.html for every path, with one title and a canonical that points home. Crawlers treat other addresses as copies of that document. Prerender or server-render each public route so the first HTML already names that page and contains its text.

Will enabling JavaScript rendering in Search Console fix an empty SPA?

It may help some crawls, but many fetches still skip scripts or time out. Ranking starts from the first HTML. If that HTML is an empty shell, you should not rely on later rendering. Save a full snapshot per route, then inspect it without running JavaScript.

What should the canonical tag be on each SPA route?

It should be the preferred public URL of that route, not the homepage, unless the route is truly a duplicate. Every indexable path needs its own self-canonical in the first HTML. Identical canonicals across paths are why search engines fold those URLs into one listing.

How do I confirm prerendering worked without a browser?

Use a command-line HTTP client on two different paths. Check that titles differ, canonicals match each path, and body text is present before script tags. If both responses are the same file, the server is still serving one shell. Fix the mapping, then fetch again.

If you are shipping a public site from a build, read the Help Centre for GROX Code deploy checks, then still fetch each live path without JavaScript.