JavaScript SEO: How to Get JS Sites Indexed

JavaScript SEO: How to Get JS Sites Indexed

Most JavaScript SEO advice starts from a fear that’s a decade out of date — the belief that Google can’t run JavaScript at all. It can, and it does, on nearly every page it crawls. The real problem is subtler and far more expensive: Google renders your JavaScript on its own schedule, with a real budget, and if your content only exists after a script executes, a single failed request, a blocked resource, or a router that speaks the wrong dialect can leave the page in the index as a blank shell. JavaScript SEO isn’t about whether Google runs your code. It’s about making sure the version Google eventually sees is the version that ranks.

Crawling, Rendering, and Indexing Are Three Different Steps

Almost every JavaScript SEO mistake traces back to blurring three distinct stages. Crawling is Googlebot fetching the raw HTML response your server sends — nothing has executed yet. Rendering is a separate service running that page’s JavaScript in a headless browser to build the final DOM, the way a user’s browser would. Indexing is Google parsing the rendered result, extracting content and links, and deciding whether the page earns a place in results.

On a server-rendered site these steps collapse into one — the HTML Googlebot fetches already contains the content, so rendering adds little and indexing happens fast. On a client-rendered site, the crawled HTML is often an empty <div id="root"></div> plus a script bundle. Everything a searcher cares about — the headline, the copy, the internal links — only appears after rendering. If you conflate “Google crawled my page” with “Google indexed my content,” you’ll misdiagnose every problem that follows.

How Googlebot Actually Renders JavaScript

Googlebot uses an evergreen, headless Chromium — the same engine as current Chrome, kept up to date — so modern APIs and syntax generally work. When it crawls a page, the HTML goes into indexing immediately, but any JavaScript-dependent content is deferred to a render queue. A rendering service picks pages off that queue, executes the scripts, and feeds the resulting DOM back for indexing.

The old framing of “two waves of indexing” days apart is largely obsolete — Google now renders most pages within seconds to minutes of crawling. But “usually fast” is not “guaranteed.” Under crawl-budget pressure, on large or slow sites, that gap can stretch, and rendering can be skipped or truncated if the page is too heavy, too slow, or throws errors. The practical rule: content that lives only in JavaScript is content Google indexes late and conditionally. Anything you need ranked reliably should not depend on a render step completing perfectly.

Why Client-Side Rendering Is the Riskiest Default

Pure client-side rendering (CSR) — the default for a plain create-react-app-style single-page app — ships an almost-empty HTML document and builds the page in the browser. It’s the most fragile pattern for client-side rendering SEO because everything that matters is gated behind successful script execution. If the bundle fails to load, a third-party API times out during render, or a component throws, the user might see a spinner and retry — Googlebot just indexes whatever the DOM contained when rendering stopped, which can be nothing.

CSR also front-loads a heavy JavaScript payload, which slows the very render step you’re depending on and drags down Core Web Vitals. For a brochure site or a blog, CSR is almost always the wrong call. It earns its place for logged-in app views behind authentication that never need to rank — not for the public, indexable surface of your site.

SSR, SSG, and Hydration: What Actually Fixes It

The durable fix in JavaScript SEO is to make sure the important content exists in the HTML before any script runs. Two approaches do this:

  • Server-side rendering (SSR): the server executes your framework and returns fully-formed HTML on each request, then the client “hydrates” it to add interactivity. Google gets real content on the first fetch.
  • Static site generation (SSG): pages are pre-rendered to HTML at build time and served as static files. Fastest to crawl, ideal for content that doesn’t change per request.

Frameworks like Next.js, Nuxt, Astro, and SvelteKit make SSR/SSG the path of least resistance, and modern setups mix them per route — static for the blog, server-rendered for personalized pages. Hydration is where teams still trip: if the server HTML and the client render disagree, React or Vue may throw a hydration mismatch and, in some configurations, discard and re-render the DOM. Google indexes the rendered result, so a mismatch that blanks content is an SEO bug, not just a console warning. Test the rendered output, not the pre-hydration source.

Dynamic Rendering Is a Deprecated Crutch, Not a Strategy

Dynamic rendering — detecting bots and serving them a separately pre-rendered HTML version while users get the JS app — was Google’s official workaround years ago. Google now explicitly calls it a workaround, not a recommended long-term solution. It’s brittle: you maintain two rendering paths, the bot version drifts out of sync with what users see (which edges toward cloaking), and prerender caches serve stale content. If you’re building fresh, invest in SSR or SSG instead. Dynamic rendering is only worth considering as a temporary bridge for a legacy stack you genuinely can’t migrate yet.

The Links and Routing Mistakes That Hide Pages From Google

You can render perfect content and still be invisible if Google can’t follow your links or reach your URLs. Two failures dominate:

  • Non-anchor “links.” Googlebot discovers URLs from <a href="/page"> elements. A <div onclick="navigate()"> or a button that changes routes via JavaScript is not a crawlable link — the destination may never be found. Always render real anchors with real href attributes.
  • Fragment routing. Old hash-based routes (example.com/#/products) are a dead end — Google ignores the fragment, so every “page” collapses to one URL. Use the History API so each view has a genuine, server-resolvable path (example.com/products).

SPAs also produce soft 404s: a deleted product route returns HTTP 200 with a “not found” message rendered in JavaScript. Google sees a successful status code and may index the empty state. Route genuine not-found conditions through a real server response — a proper 404 status, or a redirect — rather than a client-side message on a 200 page.

Don’t Block Your Own JavaScript

This is the most common self-inflicted wound in rendering SEO. If your robots.txt disallows the directories your JS bundles or JSON APIs live in, Googlebot can’t fetch the resources it needs to render the page — so it renders a broken, contentless version. A safe baseline keeps critical resources crawlable:

User-agent: *
Disallow: /admin/
Allow: /_next/static/
Allow: /assets/js/

The other trap is confusing noindex with disallow. They do different jobs. Disallow in robots.txt stops crawling; noindex (a meta tag or X-Robots-Tag header) tells Google to keep a crawlable page out of the index. The fatal combination is disallowing a URL you also want de-indexed: because Google can’t crawl it, it never sees the noindex, and the URL can linger in results as a bare link. To remove a page, let Google crawl it and serve a noindex — don’t block it. And beware JavaScript that injects a noindex tag at runtime: if Google renders it, it will honor it, so an errant script can silently deindex real pages.

How to Test Whether Your JS Content Is Actually Indexed

Never assume — verify what Google’s renderer sees, which is often not what your browser shows. A reliable testing loop:

  • View source vs. inspected DOM. “View source” shows the raw crawled HTML; DevTools’ Elements panel shows the rendered DOM. If your content is in the second but not the first, it’s JavaScript-dependent — the thing to stress-test.
  • GSC URL Inspection. The URL Inspection tool in Google Search Console shows the rendered HTML, a screenshot, and any resources Google couldn’t load. This is the closest thing to seeing through Googlebot’s eyes; the “crawled” vs “rendered” HTML diff is where JS SEO bugs surface.
  • Rich Results Test / Mobile-Friendly-style live render. Google’s live testing tools render the page on demand and reveal JS errors and blocked resources.
  • The site: and quoted-text check. Search a unique sentence from your JS-rendered content in quotes. If Google returns the page, that text was indexed; if not, it wasn’t.

JavaScript, Core Web Vitals, and INP

Heavy JavaScript is the leading cause of poor Core Web Vitals, and the vitals feed both ranking and how efficiently Google renders you. Get the current thresholds right: LCP ≤ 2.5s, CLS ≤ 0.1, and INP ≤ 200ms are the “good” bars. Note that INP (Interaction to Next Paint) replaced FID as a Core Web Vital in March 2024 — and INP is precisely the metric a bloated main thread wrecks, because it measures the delay between a user’s interaction and the next visual update. Long JavaScript tasks that block the main thread tank INP directly. Code-split aggressively, defer non-critical scripts, ship less JavaScript, and lean on SSR/SSG so the first paint isn’t waiting on a bundle. Field data in the Chrome UX Report (CrUX) is what Google actually uses, so validate against real-user numbers, not just lab scores.

Where a Continuous Audit Beats a One-Off Crawl

The hardest part of JavaScript SEO isn’t fixing any single issue — it’s catching regressions. A deploy that changes a route, blocks a bundle, or breaks hydration can silently strip content from the index between your quarterly manual crawls. Desktop crawlers like Screaming Frog are excellent for a deep, on-demand render audit, and for the deepest log-file and enterprise-scale work a dedicated crawler still earns its keep. But most teams run those tools rarely. SEO Rocket is built as the continuous layer: its real-crawler site audit surfaces indexability problems — broken links, soft 404s, bad status codes, missing or malformed schema, thin pages, redirect chains — on an ongoing basis, with the fix explained in plain language rather than buried in a spreadsheet.

Paired with rank tracking and AI-visibility monitoring on a client dashboard, that turns “did last week’s release break indexing?” from a page-by-page manual check into an alert. The point isn’t to out-crawl a specialist tool; it’s to make sure a JavaScript regression doesn’t quietly cost you three months of traffic before anyone notices — the same discipline behind a playbook proven across 1,000,000+ ranking pages.

A JavaScript SEO Checklist That Holds Up

Boil the above into a sequence you can actually run before and after every release:

  • Confirm critical content is in the rendered DOM and, ideally, the initial HTML (SSR/SSG for anything indexable).
  • Use real <a href> links and History-API routes — no hash routing, no click-handler navigation.
  • Keep JS, CSS, and API resources crawlable in robots.txt; render server-side status codes for real 404s and redirects.
  • Serve noindex only on crawlable URLs; never block a page you also want removed.
  • Ship canonical tags, titles, and meta descriptions in the server HTML, not injected late by script.
  • Watch INP, LCP, and CLS in CrUX; cut and defer JavaScript until the field data clears the thresholds.
  • Verify with GSC URL Inspection after every meaningful deploy, and monitor continuously for regressions.

Frequently Asked Questions

Can Google index JavaScript content?

Yes. Googlebot renders JavaScript using an evergreen headless Chromium and indexes the resulting content. The caveats are timing and reliability: rendering happens in a queue that can lag on large or slow sites, and it can be truncated if the page errors, blocks its own resources, or is too heavy. Content that only exists after JavaScript runs is indexed later and less reliably than content already in the server HTML.

Is server-side rendering better than client-side rendering for SEO?

For anything you need ranked, yes. SSR and SSG put real content in the HTML before any script executes, so Google indexes it immediately and reliably, with faster Core Web Vitals as a bonus. Pure client-side rendering gates all content behind a render step that can fail silently. Reserve CSR for authenticated app views that never need to appear in search.

How do I check what Googlebot renders on my page?

Use the URL Inspection tool in Google Search Console — it shows the rendered HTML, a screenshot, and any resources Google couldn’t load. Compare that rendered output against “view source” (the raw crawled HTML). If your content appears in the rendered DOM but not the source, it’s JavaScript-dependent, which is exactly where you should focus testing and hardening.

Questions? Chat with us