JavaScript SEO is the practice of making sure content rendered by JavaScript is visible to search engines, which do not experience your site the way a browser does. Google can render JavaScript, but it does so in a second pass that is slower and not guaranteed, and other crawlers do far less of it. So the practical question is never whether a JS site can rank, because it can. The real question is whether your specific content and links survive the rendering process intact and arrive in the index.

The rendering gap

The core issue is that the raw HTML a crawler first downloads may be nearly empty, with the real content injected later by scripts. If a crawler reads only that initial response and never returns for the render, it sees a blank shell where your page should be.

  • View the page source, not the rendered DOM, to see what crawlers actually get on the first fetch.
  • Test the rendered output with a tool that mimics the crawler, not just your own browser, which is far more forgiving.
  • Remember that rendering is queued and can lag by hours or days, so time-sensitive content may be missed entirely.

Googlebot’s rendering queue is real and not trivially short. Google has said publicly that rendering happens after the initial crawl and that the gap can range from hours to weeks depending on how the site is prioritized. For content that needs to rank quickly, that lag is a genuine problem, not a theoretical one. I have seen news sites running React lose entire news cycles because the article body was not in the initial HTML response.

How to check what Googlebot actually sees

Before you assume your content is rendering correctly, verify it. There are a few ways to do this without guessing.

  • Use Google Search Console’s URL Inspection tool and click "Test Live URL" to see a screenshot of how Googlebot renders the page at that moment.
  • Run Screaming Frog with JavaScript rendering enabled and compare the rendered crawl against a raw HTML crawl. Anything in the rendered version but absent from the raw version is invisible to crawlers that do not render.
  • Check your server log files for Googlebot visits and note how frequently different page types are being re-crawled. A page that was crawled once six months ago and never revisited will not benefit from a render that happens days after each crawl.

Links and routing

Crawlers follow links by reading a href values in the HTML. JavaScript click handlers that change the view without producing a real URL are effectively invisible to them, which means whole sections can go undiscovered.

  • Use real a href anchors for navigation, not a span or div wired up with an onclick handler.
  • Make sure client-side routing produces distinct, crawlable URLs that return real content when requested directly.
  • Do not block your JavaScript or CSS files in robots.txt, or the crawler cannot render the page at all and is left with the empty shell.

The robots.txt point here is more common than it should be. I still find sites where the team blocked /static/js/ to prevent crawlers from "wasting time" on script files, without realizing that those files are what Googlebot needs to render the page. The result is that every page on the site crawls as an empty document, and the index coverage report in Search Console fills up with "Crawled but not indexed" entries that look mysterious until you check the raw source.

Choosing a rendering strategy

The most reliable fix is to deliver content in the initial HTML response wherever you can, so nothing depends on a render that may never happen. Related: hreflang tags.

  • Server-side rendering sends fully formed HTML and is the safest option for content that must rank.
  • Static generation pre-builds pages at deploy time, which is ideal for content that does not change per request.
  • Dynamic rendering serves a pre-rendered version to crawlers as a stopgap, though I treat it as a bridge rather than a permanent home.

Dynamic rendering deserves a more honest assessment than it usually gets. It works: you detect the Googlebot user agent and serve a pre-rendered static snapshot instead of the JavaScript application. The coverage and ranking improvements are real. But it also means maintaining two rendering pipelines, and any desync between them means crawlers index a version of your content that does not match what users see. Google has discouraged it as a long-term architecture for that reason. Use it if migration to SSR is not immediately feasible, but plan the migration.

Status codes and the JavaScript trap

One of the more damaging patterns in JavaScript-heavy sites is the soft 404. A user requests a page for a product that no longer exists. The server returns a 200 status with a shell HTML document, and the JavaScript application renders a "page not found" message client-side. From Googlebot’s perspective, it received a 200 status code and a document. It indexes the empty shell as a live page.

The fix is to return the correct HTTP status code from the server, not from the client-side application. A missing product should return a 404. A moved product should return a 301 to the new URL. These are server-level decisions that should not depend on JavaScript executing. If your framework makes this difficult, it is worth the engineering effort to solve it, because soft 404s at scale quietly fill your index with useless pages and dilute the signals your real pages should be collecting.

Common failure points to check

When a JavaScript site underperforms in search, the cause is usually one of a small set of recurring issues rather than anything exotic. Related: content audit. I work through these first before reaching for anything more involved.

  • Content that only loads after a user interaction, such as clicking a tab or accepting a consent banner, which a crawler never triggers.
  • Metadata like titles and descriptions injected by script, which can be missed or duplicated if the render is skipped.
  • Soft 404s, where a missing page returns a 200 status with an empty shell instead of a proper error code.
  • Slow or heavy bundles that push rendering past the point where the crawler gives up waiting.

If your framework lazy-loads paginated lists, the same rendering rules apply to those deeper pages, which is why this overlaps directly with how you structure paged content. I cover that in pagination and SEO. If you run a JS-heavy site and are unsure what is actually reaching the index, I can pull the rendered versus raw HTML and pinpoint the gaps in a website audit.