There is a version of your website that only search engines see, and if your site leans on JavaScript — a React or Vue frontend, a page builder that assembles content client-side, widgets injecting your reviews and service lists, tag-manager scripts “enhancing” the page after load — that version may be missing the parts you care about most. The browser you test in executes everything instantly and shows you the finished page; a crawler first sees the raw HTML your server sent, and only later — sometimes much later, sometimes imperfectly — the rendered result after JavaScript runs. In the gap between those two views live an entire genre of invisible failures: service pages whose content exists only after a framework hydrates, internal links rendered as click-handlers no crawler follows, titles and canonicals rewritten client-side into conflict with what the server said, and review widgets whose social proof is, to the index, an empty div.

The reassuring half of the story: Google renders JavaScript, at scale, with an evergreen Chromium — modern JS sites can and do rank. The unreassuring half: rendering happens in a second wave after crawling (the two-phase reality — raw HTML first, rendered evaluation later, with delays that vary), it can fail partially and silently (blocked resources, errors, timeouts, content behind interactions that never happen), other search and AI crawlers render far less or not at all — and the failure mode isn’t an error message but a quiet underperformance nobody attributes to rendering. Which means JavaScript SEO for a business site isn’t a framework debate; it’s a verification discipline: knowing how to check what crawlers actually receive, which failure patterns to look for, and which architectural choices make the whole question boring.

This guide is that discipline: the two-phase crawling-and-rendering reality and what it implies, the verification toolkit — URL Inspection’s rendered HTML, the JS-off test, source-vs-rendered diffs, and the crawler-based audit at scale — the failure catalog (content, links, metadata, and the interaction-gated content trap), the fix hierarchy from server-side rendering down to targeted patches, the special stakes for local businesses and the widget economy they live in, and the AI-crawler wrinkle that’s quietly raising the price of client-side-only content.

TL;DR · Quick Summary

Google crawls in two phases: raw HTML first, rendered JavaScript later — rendering works but is deferred, can fail silently, and most other crawlers (including many AI crawlers) barely render at all. Verify, don’t assume: URL Inspection → View crawled page → compare the rendered HTML against what you expect (is the content there? the links? the meta?); the JS-off test (disable JavaScript, reload) shows what non-rendering crawlers get; a crawler run in JS-rendering mode diffs source vs rendered site-wide — flagging content, links, titles, and canonicals that exist only post-render. The failure catalog: core content client-side only; internal links as click-handlers instead of real <a href> elements (crawlers follow hrefs, not onClicks); metadata injected or rewritten by JS into conflict with the server’s; content gated behind clicks/scrolls/tabs that renderers never perform; blocked JS/CSS resources breaking rendering. The fix hierarchy: server-side rendering or static generation for anything that must rank (content and links in the initial HTML); hydration on top is fine; client-side-only rendering reserved for app-like surfaces that don’t need to rank. Local stakes: review widgets, service lists, and booking elements are the usual invisible content — and AI answer engines’ weaker rendering makes initial-HTML content the safe bet for the next era.

What Each Visitor Actually Receives · rendering reality What Each Visitor Actually Receives · rendering reality Share of a JS-heavy page’s content visible to each consumer (illustrative model) Human in a browser · full renderwhat you testGooglebot · after deferred renderingusually, eventuallyGooglebot · raw HTML first passwhat indexing starts fromOther search crawlers · little renderingbing varies, most don'tAI answer-engine crawlers · mostly raw HTMLthe new stakes Illustrative model · mantasauk.com

The Two-Phase Reality — and Its Four Practical Implications

Google’s pipeline: crawl fetches your raw server HTML; indexing begins from that; pages needing JavaScript enter a render queue where an evergreen Chromium executes them; the rendered result then updates the evaluation. It works — and four properties shape everything you should do about it:

  • Deferral: rendering happens after crawling — historically anywhere from seconds to much longer, and while the gap has shrunk, it exists. New and frequently-changing content that lives only post-render is evaluated late; content in the initial HTML is evaluated immediately.
  • Fragility: the renderer times out, skips interactions, respects robots.txt on your JS/CSS files (blocked resources = broken renders), and gives up on errors your users’ browsers survive. Failures don’t alert; they just leave the raw-HTML version as the truth.
  • Asymmetry across crawlers: Googlebot renders well; Bing renders selectively; most other bots — and, critically, most AI answer-engine crawlers assembling responses from the web — work substantially from raw HTML. A client-side-only site is betting its entire non-Google visibility on a capability those consumers don’t have.
  • Budget: rendering is expensive, and heavy JS effectively spends more of your crawl attention per page — a large-site concern mostly, but one more thumb on the scale for initial-HTML content.

The strategic conclusion, before any testing: content and links that must rank belong in the initial HTML. Everything else — interactivity, personalization, app-like behavior — can be JavaScript’s job. That single sentence is most of JavaScript SEO; the rest of this guide is verifying whether your site honors it and fixing where it doesn’t.

The Verification Toolkit, Cheapest First

  1. The JS-off test (2 minutes): disable JavaScript in your browser (DevTools or an extension), load your money pages. What survives is roughly what non-rendering crawlers receive — if your service page becomes a header and a spinner, you’ve learned the most important fact about your site’s technical SEO in two minutes. Not a perfect Googlebot simulation (Google renders); a perfect everything-else simulation.
  2. Source vs. rendered (5 minutes): view-source (the server’s HTML) versus DevTools’ Elements panel (the rendered DOM) on key templates. Search both for a distinctive content sentence, a key internal link’s href, the title, the canonical: anything present only in Elements is JavaScript-delivered — inventory it.
  3. URL Inspection’s rendered view (the ground truth for Google): Search Console → URL Inspection → Test live URL → View tested page: the HTML tab shows what Google’s renderer actually produced, the screenshot shows the visual, and “More info” lists resources that failed to load (the blocked-JS smoking gun) and console errors. Check: the money content present in the rendered HTML? the internal links as real anchors? the metadata as intended? This is the authoritative per-page answer, template by template.
  4. The crawler diff at scale: a site crawler run twice — raw HTML mode, then JavaScript-rendering mode — and diffed: word counts, link counts, titles, canonicals per URL across the two runs. Pages whose content or links exist meaningfully only in the rendered run are your exposure list, ranked by the same value annotations as any migration audit. This is also the check that belongs in every redesign QA where the new stack is JS-heavier than the old.
  5. Corroborating signals in the wild: a site: search quoting a JS-delivered sentence (indexed or not?), Search Console’s crawl stats and page-indexing patterns (JS-dependent templates underperforming their linked, quality-equivalent peers), and cache/snippet checks showing what actually made it in.
Test the Templates, Not the Site

JavaScript delivery is a property of templates, not URLs — your service-page template either puts content in the initial HTML or it doesn’t, across all forty service pages identically. So the verification workload is small: one representative URL per template (home, service, city, article, category) through the JS-off test and URL Inspection, and you’ve characterized the whole site in under an hour. The per-URL crawler diff earns its time only when templates mix behaviors (CMS pages fine, builder pages broken) or when you need the exposure inventory for prioritizing fixes — and the template insight tells you that too.

The Failure Catalog

FailureWhat it looks likeThe fix
Core content client-side onlyRaw HTML is a shell (<div id="root">); everything meaningful arrives post-hydration — the SPA defaultSSR/SSG for the ranking surfaces (below); the content in the initial response
Links that aren’t linksNavigation via onClick handlers, buttons, or router calls without real <a href> elements — crawlers discover by following hrefs; handler-navigation is invisible, and whole sections go functionally orphanedReal anchors with real hrefs everywhere navigation happens (frameworks’ Link components do emit them — verify in the rendered HTML that they did)
Metadata written or rewritten by JSTitles, descriptions, canonicals, robots meta injected client-side — evaluated only after rendering, ignored by non-renderers, and worst when they conflict with the server’s versions (two canonicals, two titles — signal chaos per the consistency rules)Metadata server-side, period; if a framework head-manager is used, SSR it so the initial HTML carries the final values
Interaction-gated contentTabs, accordions, “load more,” and infinite scroll whose content only enters the DOM on click/scroll — renderers don’t click and scroll only in limited ways; unloaded content doesn’t existContent in the DOM at load (hidden-but-present via CSS is fine and indexed); paginated real URLs behind “load more”; never DOM-on-demand for anything that should rank
Blocked resourcesrobots.txt disallowing /js/, /assets/, or the CDN paths — the renderer can’t execute what it can’t fetch; pages render as broken shellsUnblock JS/CSS for crawlers (a legacy-robots.txt classic); URL Inspection’s resource list is the detector
Render-breaking errors & timeoutsScripts erroring in the renderer’s environment (missing browser APIs, third-party failures) or hydration so slow the renderer captures the pre-content stateError monitoring that includes bot traffic; graceful degradation — the page’s content shouldn’t depend on the most fragile script finishing
The one-sentence architecture rule “If it should rank, it ships in the initial HTML — content, links, and metadata. JavaScript then enhances a page that was already complete, instead of constructing one that was empty. Every rendering question dissolves under that rule; every rendering problem is a violation of it.”

The Fix Hierarchy

  1. Server-side rendering (SSR) or static generation (SSG) for every ranking surface: the frameworks all support it (Next/Nuxt/SvelteKit and peers exist substantially for this), and static generation in particular — pages pre-built as complete HTML — is both the fastest and the most crawler-proof delivery there is; hydration adds the interactivity on top. For a service business’s pages, articles, and city pages, SSG is almost always the right answer and comes with the performance dividend free.
  2. Prerendering as the retrofit: where re-architecting isn’t feasible, prerendering services/middleware serve crawlers a rendered snapshot — legitimate (serving equivalent content to bots is fine; cloaking is serving different content), workable, but a maintenance liability layer that modern dynamic-rendering guidance treats as a workaround rather than a destination.
  3. Targeted patches for specific failures: real hrefs replacing handler-navigation; metadata moved server-side; accordion content in-DOM; the robots.txt unblock; the third-party widget’s content mirrored in HTML (below). Often the exposure list from the crawler diff is five patches, not a replatform.
  4. Acceptance for app surfaces: dashboards, portals, booking flows, calculators — client-side rendering is fine for surfaces that shouldn’t rank; give them clean noindex/robots treatment per the decision tree and spend the SSR effort where rankings live.
The Local-Business Version: Widgets, Builders, and the Reviews That Aren’t There

Service businesses rarely run bespoke SPAs — their JavaScript exposure arrives through the supply chain. Review widgets that inject testimonials client-side from a third-party script: the social proof your conversion depends on may be an empty container in the index (and in every AI crawler’s view) — check whether the widget offers a server-side or HTML-embed mode, or mirror key reviews as real HTML content with proper markup. Page builders and themes that assemble sections client-side: the JS-off test per template tells you in minutes. Store locators and service-area maps rendering the city list only in JS: the city links crawlers were supposed to follow don’t exist — a real HTML list alongside the map fixes discovery. Tag-manager ‘SEO’ injections (titles, schema, even content edits via GTM): evaluated late by Google, never by anyone else, and a standing conflict risk with the server’s HTML — GTM is for tags, not for content. The audit habit: every third-party element on a money page gets the view-source check — is what this widget shows actually in my HTML? — because your rankings inherit every vendor’s rendering decisions.

The AI-Crawler Wrinkle

The consumers of your HTML now include the crawlers feeding AI answer engines — and their rendering capabilities are, broadly, closer to the raw-HTML era than to Googlebot’s Chromium: content that exists only post-render is content that largely doesn’t exist for the systems assembling AI answers, citations, and recommendations. For businesses investing in answer-engine visibility, this quietly re-raises the stakes of the initial-HTML rule: the FAQ content, the service descriptions, the expertise signals you want quoted and cited need to be server-delivered, structured, and present without execution. The convenient alignment: everything that serves AI crawlers — complete initial HTML, real links, honest metadata, structured data server-side — is identical to what serves Google’s first-pass indexing, other search engines, accessibility, and raw speed. The initial-HTML rule was good engineering before; the AI era just added another paying customer for it.

5 Common JavaScript SEO Mistakes

  1. Testing in a browser and calling it verified. Your browser renders everything; the question is what arrives before rendering — view-source and the JS-off test, not the Elements panel.
  2. “Google renders JS” as a full answer. True, deferred, fragile at the edges — and irrelevant to every other crawler now assembling answers about your business.
  3. Handler-navigation. Click-routing without hrefs orphans sections invisibly — the single most common JS-site link failure.
  4. GTM as a content-management system. Client-side metadata and content injection conflicts with the server’s HTML and reaches only the most patient crawler.
  5. Auditing once. Every theme update, new widget, and framework migration re-rolls the dice — the template checks belong in redesign QA and the quarterly technical sweep, permanently.

Frequently Asked Questions

Our site is built in React/Vue. Are we automatically at an SEO disadvantage?

Not automatically — the framework isn’t the risk; the rendering strategy is. A React or Vue site delivered via server-side rendering or static generation (Next, Nuxt, and their peers exist precisely for this) ships complete HTML with content, links, and metadata in the initial response — crawlers of every capability level receive a finished page, and the framework’s client-side behavior is pure enhancement; such sites compete without handicap and often with a speed advantage. The disadvantaged configuration is client-side-only rendering for ranking surfaces: the empty-shell initial HTML that depends entirely on Google’s deferred renderer and is simply invisible to lesser crawlers, including much of the AI-answer ecosystem. The verification is empirical, not ideological: run your money templates through view-source and the JS-off test — if the content and real anchor links are in the initial HTML, your framework choice is an implementation detail; if they’re not, the fix is turning on the rendering mode your framework already supports (SSG for content pages being the usual right answer), not abandoning the stack. The one standing discipline for framework sites: re-verify after major upgrades and refactors, because rendering strategy is a build configuration — and configurations drift.

What's the fastest way to check if Google sees my page correctly, right now?

The five-minute sequence, in order of authority. One: URL Inspection in Search Console on the page in question → Test live URL → View tested page — read the rendered HTML tab and search it (ctrl-F) for a distinctive sentence of your main content, a key internal link’s URL, and your title/canonical; then check the ‘More info’ panel for resources that couldn’t load and console errors. Present and clean = Google’s renderer handles the page; this is the ground truth. Two: view-source on the same page and repeat the searches — this tells you whether the content lives in the initial HTML (robust across all crawlers) or only post-render (dependent on rendering); the distinction matters even when test one passed. Three: the JS-off reload for the non-Google world — what remains is roughly what AI crawlers and lesser bots receive. Corroborate at leisure with a site: search quoting a JS-delivered phrase (did it actually get indexed?) and, for anything systematic, the two-mode crawler diff. The five minutes answers the acute question; the template-by-template pass answers it for the site.

Our reviews widget loads testimonials with JavaScript. Does it matter for rankings?

It matters on three layers, in ascending order of certainty. Indexing: client-injected review content may or may not make it into Google’s evaluation of the page (deferred rendering usually captures it; third-party script failures, timeouts, and blocked resources sometimes don’t) — and it definitively doesn’t exist for non-rendering crawlers, including most of the AI-answer ecosystem now summarizing ‘what people say about’ businesses; social proof that isn’t in your HTML can’t be quoted from it. Structured data: review markup injected client-side is the least reliable way to deliver it — schema belongs in the server HTML, and note the policy layer: self-serving review snippets (LocalBusiness rich stars on your own site’s reviews of itself) are restricted regardless of delivery method, so the markup question needs the compliance answer too. Conversion resilience: a widget dependency means your money pages’ most persuasive element fails whenever the vendor hiccups. The practical fix stack: prefer widgets offering server-side/HTML embed modes; otherwise mirror a curated set of genuine reviews as real HTML content (quotes, names as permitted, dates) with the widget as the live-updating supplement; and keep the canonical review mass where it does the most ranking work anyway — on the Business Profile, per the local-SEO playbooks — with the site’s job being to reflect it in crawlable form.

Is dynamic rendering / prerendering for bots considered cloaking?

No — provided the prerendered snapshot is substantively the same content a user receives, serving crawlers a rendered version of your JavaScript page is a recognized workaround, not deception; cloaking is showing bots different content (keywords users never see, pages users can’t reach), and the intent-and-equivalence line is what separates them. That said, the current guidance treats dynamic rendering as transitional rather than a destination, and the operational record explains why: prerendering layers are a second rendering pipeline to maintain — snapshots go stale, the prerenderer fails silently while the user site looks fine (inverting the usual failure visibility), bot-detection lists need upkeep as crawler user-agents evolve, and every framework upgrade risks divergence between the two versions. The decision framing: prerendering is a reasonable bridge when a client-side-only architecture can’t be re-platformed this quarter and the exposure is material — and a poor permanent home compared to SSR/SSG, which solves the same problem in one pipeline with performance benefits attached. If you do run it: monitor the snapshots themselves (scheduled URL Inspection checks on key templates), keep the equivalence honest, and put the migration to real server rendering on the roadmap rather than in the someday pile.

How do 'load more' buttons and infinite scroll affect what gets indexed?

Content that enters the DOM only when a user clicks or scrolls generally doesn’t exist for indexing — renderers don’t click, and scroll simulation is limited — so an article list, product grid, or review stream behind interaction-loading is indexed only as deep as the initial payload; everything past it is invisible, and anything linked only from the invisible portion inherits functional orphanhood. The crawlable pattern is pagination with real URLs underneath the interaction layer: each page of content reachable at its own address (?page=2 or /page/2/) via real anchor links present in the HTML — the ‘load more’ button can then be the enhanced experience on top (fetching the next page’s content in place) while crawlers follow the underlying links; this hybrid is the standard recommendation and frameworks implement it readily. Distinguish the neighboring case: content present in the DOM at load but visually hidden — tabs, accordions, read-more toggles that merely toggle CSS — is indexed fine (hidden-but-present content is evaluated; this pattern is safe and standard for FAQs). The test that settles any specific implementation in thirty seconds: load the page, view the rendered DOM before any interaction, and search for content from the second ‘page’ and from inside the collapsed sections — present means indexable, absent means the interaction is a wall; and the JS-off version of the same test tells you whether the underlying pagination links exist for the non-rendering world.

Is the version of your site that crawlers see the one you built?

We’ll run the full rendering audit — template-by-template verification, the source-vs-rendered diff, the widget supply-chain check — and implement the fix hierarchy so your content exists for every crawler that matters, including the AI ones.

Get a Rendering Audit Explore AEO Services