JavaScript-Rendered Content Not Showing in Google Index

Your SPA renders fine for users, but Search Console shows the page indexed with an empty body. Why Googlebot's render pass loses your content, and the fastest fix.

You ship a React/Vue/Svelte SPA. The URL is indexed in Google. But search a unique phrase from the page body and nothing comes up. Open Search Console, run URL Inspection, click Test live URL, then View tested page, then the HTML tab. The body is nearly empty, or contains only a loading spinner. Googlebot got the shell and never the content. The same problem shows up as “Indexed but no impressions,” a page that ranks for nothing, or rich results that fail validation for content that is visibly on the page.

Fastest fix: if the content matters for search, render it on the server (SSR) or pre-build it (SSG) so it’s present in the raw HTML, not painted later by JavaScript. Everything below is for diagnosing why the render pass is failing when you can’t move to SSR/SSG immediately.

How Googlebot actually renders JavaScript (as of June 2026)

Google indexes JS pages in two passes:

  1. Crawl — Googlebot fetches the raw HTML and indexes whatever text, links, and metadata are already in that response.
  2. Render — the page is queued for the Web Rendering Service (WRS), an evergreen headless Chromium. It executes your JavaScript, waits for the network to settle, and re-reads the resulting DOM.

Two facts decide whether your content survives this:

  • There is no hard “5-second render limit.” That number is a myth that comes from an old median statistic. Google’s Martin Splitt has said pages are rendered within minutes about 99% of the time, and the often-quoted 5 seconds was the median crawl-to-render gap, not a per-page cutoff. So “just wait longer” is almost never the real fix. If content is missing, the render failed or never fired, not “timed out.”
  • WRS does not interact with the page. It does not click, scroll, hover, switch tabs, or sit idle long enough to trigger lazy work. Per Google’s own JavaScript SEO guidance, content that only appears after a user action is not seen. This is the single most common cause below.

Common causes

1. Content fetched only after a user event

Your component looks like it fetches on mount with useEffect(() => fetch('/api/article')), but it actually fires on a requestIdleCallback, a scroll handler, an IntersectionObserver, or a click. Googlebot never triggers any of those.

How to spot it: in Chrome DevTools, open the page, then in the Console run a quick check that no scroll or idle callback is gating your fetch. Or set Network throttling to “Slow 4G,” do not touch the page, and wait 5 seconds. If the body never paints on its own, Googlebot sees the same blank.

2. The render API is blocked by robots.txt

The HTML loads, then JS calls /api/data. But /api/* sits under Disallow: in robots.txt. Google’s documentation is explicit: “Google Search won’t render JavaScript from blocked files or on blocked pages.” No API response means no rendered content.

How to spot it: open https://yoursite.com/robots.txt and check that the data endpoints your page renders from are not disallowed. In URL Inspection, View tested page → More info → Page resources lists every fetch Googlebot attempted and flags the ones it could not load.

3. A JavaScript error breaks the render

A runtime error like Cannot read properties of undefined (reading 'map') halts hydration. The shell paints, the data layer never does. WRS does not retry your app; one uncaught throw and the content is gone.

How to spot it: in URL Inspection run a live test, then View tested page → More info → JavaScript console messages. (This panel and the rendered screenshot are only populated on a live test, not the indexed snapshot.) Any error there is your render dying.

4. The render is too heavy or too slow to finish

WRS is not infinitely patient. Render-blocking third-party scripts, multi-megabyte bundles, or a chain of slow API calls can leave the content un-painted when WRS captures the DOM. This is a resource problem, not a fixed clock.

How to spot it: run Lighthouse. An LCP above ~4s or Total Blocking Time above ~600ms means the critical content is slow to appear and at risk. Compare the rendered screenshot in URL Inspection against your live page.

5. The client-side router never updates the title and meta

A user navigates to /article/foo through the SPA router. The title and meta tags stay on the home-page values because the router never updated them. Googlebot indexes the second URL with home-page metadata, so the page “ranks” only for the home page’s topic.

How to spot it: click through 3 articles in a row and watch the title in the DevTools Elements panel after each navigation. If it does not change, your router is not updating document.title.

6. A service worker serves stale content to Googlebot

A PWA service worker has cached an old bundle that breaks against the current API. Logged-in users get a fresh copy via cache-busting; Googlebot fetches plain and hits the broken cached version.

How to spot it: in DevTools, open Application → Service Workers and “Unregister,” then hard-reload to see the un-cached render. Compare a request with the User-Agent set to Googlebot/2.1 against a normal browser request; differing HTML means a service worker or CDN is splitting traffic.

7. Pure CSR with no fallback for crawlers

The HTML is <div id="root"></div> plus a script tag. The render pass is the only chance to see content, so any of causes 1 through 4 means a permanent blank in the index.

How to spot it: run curl https://yoursite.com/article/foo. If the response is the same shell regardless of URL, there’s no prerendered HTML and no SSR — every page depends entirely on a flawless render.

Which bucket are you in?

Symptom in URL InspectionMost likely causeGo to
Rendered HTML has the shell but no body textFetch gated on interaction (1) or JS error (3)Steps 1, 3
Page resources shows a blocked or failed /api/... fetchrobots.txt or CORS blocking the API (2)Step 4
JavaScript console messages shows a runtime errorRender crash (3)Steps 1, 3
Rendered screenshot is blank/spinner; Lighthouse LCP highRender too slow/heavy (4)Step 3
Body indexes fine but title/meta is wrong per URLRouter not updating head (5)Step 5
curl returns identical shell for every URLPure CSR, no fallback (7)Step 2

Shortest path to fix

Step 1: Confirm exactly what Googlebot rendered

Search Console → URL Inspection → Test live URLView tested pageHTML. That tab is the DOM after Googlebot ran your JavaScript — not what curl returns and not what your browser shows. Also open More info → JavaScript console messages and Page resources in the same live test; these only populate for a live test, and they tell you immediately whether the failure is a crash, a blocked resource, or content that simply never fired.

Step 2: Render server-side or prerender

The most reliable fix is to stop relying on a client render for indexable content:

  • SSR — Next.js getServerSideProps or React Server Components, Nuxt useAsyncData, SvelteKit +page.server.ts.
  • SSG — Astro, Next.js generateStaticParams / static export, Hugo. Best for content that doesn’t change per request.
  • Prerender — Prerender.io or a self-hosted Rendertron snapshot served to crawlers. Note Google now calls dynamic rendering “a workaround and not a long-term solution,” so treat this as a bridge to SSR/SSG, not the destination.
// Next.js (Pages Router): SSR for an article page
export async function getServerSideProps({ params }) {
  const res = await fetch(`https://api.example.com/articles/${params.slug}`);
  const article = await res.json();
  return { props: { article } };
}

Step 3: Make critical content render with no interaction

Move data fetches out of requestIdleCallback, scroll handlers, and IntersectionObserver. The title, body, and structured data must be in the server response or fetched in an un-gated useEffect/onMounted that runs on load. Guard your render so a failed fetch shows real text, not a permanent spinner, and wrap data access in optional chaining so one missing field can’t throw and kill the whole render.

Step 4: Allow the API endpoints your render depends on

If a client render is unavoidable, don’t Disallow: the endpoints the page reads from:

User-agent: Googlebot
Allow: /api/articles/
Allow: /api/categories/
Disallow: /api/admin/

Same-origin APIs are simplest; if the API is on another origin, make sure it returns permissive CORS headers for the fetch, since a CORS failure looks identical to a blocked resource in the rendered output.

Step 5: Update title and meta on every client-side route change

For SPAs that stay client-side, use React Helmet / Next Head, Vue’s useHead (@unhead/vue), or update document.title and the meta tags directly on each route change:

router.afterEach((to) => {
  document.title = to.meta.title || 'Site Name';
  document
    .querySelector('meta[name="description"]')
    ?.setAttribute('content', to.meta.description || '');
});

Step 6: Test rendering at scale, not one URL at a time

A single URL Inspection won’t catch a pattern that breaks on 30% of pages. Run a JS-rendering crawl — Screaming Frog with JavaScript rendering enabled, Sitebulb, or a Puppeteer script — across 100+ URLs and diff the rendered HTML against the raw HTML. Pages with a large diff (lots of content only present after render) are the ones at risk.

Step 7: Confirm the fix, then watch Search Console

To confirm: re-run URL Inspection’s live test and verify the HTML tab now contains your body text and the correct title. For SSR/SSG, also curl the URL and confirm the content is in the raw response. Then request indexing. Expect 1 to 3 weeks before “Crawled - currently not indexed” counts fall and impressions for body-text queries appear, depending on how often Google recrawls the section.

Easy to misdiagnose as

A content-quality problem. The page looks indexed by URL, so it’s tempting to rewrite the article. But if the body never reached the index, Google has no idea what the page is about, so it ranks for nothing no matter how good the writing is. Always check the rendered HTML before touching the content.

When it isn’t your code

Some older framework setups have known Googlebot rendering quirks (legacy Angular Universal versions, old Ember). Upgrading the framework is the real fix, not patching around it. Separately, Google’s AI surfaces (Gemini and AI Overviews) now render JavaScript with the same WRS pipeline per Martin Splitt’s 2026 comments, so a CSR page that’s invisible to classic Search is also invisible to those surfaces — fixing the render fixes both.

Prevention

  • Default to SSR or SSG for anything meant to rank.
  • Never Disallow: an API your render depends on.
  • Run a monthly rendered-vs-raw HTML diff on a sample of URLs.
  • Budget JS bundle size and render time, and fail CI above the threshold.
  • For SPAs, always update document.title and meta on route change.

FAQ

  • Doesn’t Google fully render JavaScript now? It renders most JavaScript, most of the time, eventually — but the render pass is deferred, runs on a budget, does not interact with the page, and skips content that crashed. “Renders JS” is not “renders everything reliably.”
  • Is there really no 5-second timeout? No fixed per-page cutoff. The widely repeated “5 seconds” was a median crawl-to-render gap; Google has said most pages render within minutes. Missing content means the render failed or never fired, not that it “ran out of time.”
  • Will prerendering get me penalized for cloaking? No, as long as the HTML you serve crawlers matches what users see. Cloaking is showing crawlers different content. Showing the same content via a different technical path is fine.
  • Why is my body indexed but the title wrong? Your SPA router updated the content but not document.title/meta on navigation. See Step 5.
  • Googlebot fetched my page but the rendered HTML is still blank — what now? Run a live test and read JavaScript console messages and Page resources. A console error means a render crash (Step 3); a blocked or failed /api/... fetch means robots.txt or CORS (Step 4).

Tags: #SEO #Troubleshooting #Indexing #Search Console #javascript #spa #rendering