Notes from the work

JavaScript SEO: how Google renders JS and how to fix rendering issues

JavaScript SEO guide: how Googlebot crawls, renders and indexes JS, how to see what it actually rendered, and the fixes for React, Vue and Angular sites.

John KyprianouJohn Kyprianou14 min readUpdated

A page can look perfect in Chrome and still be empty to the system that decides whether it ranks. That gap is where every JavaScript SEO problem lives.

In 2026 the gap has a second edge. Google renders your JavaScript reliably. The crawlers behind ChatGPT, Claude and Perplexity do not render it at all, so a client-side rendered product page can rank on Google and be invisible to every AI answer at the same time.

Diagram of a JavaScript page passing through Googlebot's crawl, render and index phases, by SEO Turtle

This guide covers both halves: how Google actually processes JavaScript, how to see what it rendered, and the fixes that recover rankings. We have been running technical SEO audits on JavaScript-heavy sites for years, and the same handful of failures account for almost every case.

How Google processes your JavaScript

Google does not process a JavaScript page in one pass. Its own documentation describes three phases: crawling, rendering and indexing, and each one can fail independently.

Phase 1: crawling

Googlebot checks robots.txt first. If the URL is disallowed it skips the HTTP request entirely, which a robots.txt tester will confirm in seconds. If your JavaScript bundle or API endpoint is blocked, rendering cannot work either.

It then fetches the HTML and parses it for links in href attributes. On a client-side rendered app that initial HTML is often an empty shell with a <div id="root">, so Google finds nothing to queue until rendering happens. Always emit real <a href=""> tags, even with JavaScript routing.

Phase 2: rendering

Every page that returns a 200 goes into a render queue. Google's wording is deliberately vague: the page "may stay on this queue for a few seconds, but it can take longer than that."

The queue then hands the page to the Web Rendering Service, which uses an evergreen version of Chromium. That means current browser APIs are generally available, but not that every resource will load or that a slow API call will be waited for.

The old rule of thumb was a hard five-second timeout. Google has never published a number, and the five seconds that gets quoted was Martin Splitt describing how long a page typically waits in the queue, not how long the renderer waits for your API. Treat five seconds as a budget for your critical content, not a wall, and do not design around either.

Phase 3: indexing

Only after rendering does Google see your real content, links and meta tags. The failures we see most at this stage are soft 404s on single-page apps, noindex or canonical tags injected by JavaScript that conflict with the source HTML, and content that rendered but never appeared because it was gated behind a click or a permission prompt.

What changed in 2026

Three things are worth knowing before you audit anything.

Google has become more confident, and says so. On 4 March 2026 it removed the "Design for accessibility" section from its JavaScript SEO documentation, explaining that "Google Search has been rendering JavaScript for multiple years now, so using JavaScript to load content is not 'making it harder for Google Search'" (Search Engine Land). The two-wave rendering delays of 2018 are not the practical problem any more.

Dynamic rendering is officially a workaround. Google's documentation now calls it "a workaround and not a long-term solution" and recommends server-side rendering, static rendering or hydration instead. If a vendor is still selling you a prerender proxy as the fix, it is 2019 advice.

AI crawlers do not run JavaScript. Vercel's analysis of roughly 1.3 billion AI crawler fetches on its network found that GPTBot and ClaudeBot fetch JavaScript files (in 11.5% and 23.8% of requests respectively) but do not execute them, and none of the AI crawlers it tracked rendered client-side content. Glenn Gabe's August 2025 test asked ChatGPT, Perplexity and Claude to read pages from a client-side rendered site, and all three said they could not access the content, while the same pages served with server rendering worked.

Crawler Executes JavaScript What that means for a client-side rendered page
Googlebot (Search, AI Overviews, AI Mode) Yes, evergreen Chromium Content is indexed after rendering, with a delay
GPTBot and ChatGPT's fetchers No ChatGPT sees the empty shell
ClaudeBot and Claude's fetchers No Claude sees the empty shell
PerplexityBot No Perplexity sees the empty shell

The consequence for React, Next.js, Vue and Angular teams is blunt. Anything you want quoted, cited or recommended by an AI assistant has to be in the initial HTML response. We covered why this is a debt problem rather than a content problem in AI visibility technical debt, and what ChatGPT's fetcher actually reads in ChatGPT's index and cache. If you are also deciding which of those bots to allow, our AI crawlers robots.txt guide covers the user agents.

Quick diagnostic process

Before touching code, find out what each crawler actually received.

1. Look at the rendered HTML, not your browser

Chrome DevTools shows you what your logged-in browser rendered with cookies, storage and a warm cache. Googlebot has none of that.

Use the URL Inspection tool in Search Console, run Test live URL, then open View tested page. That panel gives you a screenshot of the rendered page, the raw HTML returned, the HTTP headers, the JavaScript console output and every page resource loaded. The screenshot only exists on a live test, and the console output is where you find the error that only happens for Googlebot.

Compare that rendered HTML against your view-source. If the content is in one and not the other, you have a rendering problem. If it is in neither, you have a build problem. For a fast first pass on a single URL, our Chrome SEO audit extension flags the on-page basics without leaving the page, and the Rich Results Test remains a quick way to see rendered structured data.

2. Look at the raw HTML the AI crawlers get

This one takes ten seconds and most audits skip it:

curl -s -A "GPTBot" https://www.example.com/your-product-page/ | grep -c "your product name"

If the count is zero, ChatGPT cannot see that product. Repeat with a few important templates. There is no rendering step to debug here, because none of those crawlers render.

3. Log the errors Googlebot hits

Google's troubleshooting guide includes a global error handler that ships console errors to your own endpoint. We put a version of it on every JavaScript-heavy site we work on:

window.addEventListener('error', function (e) {
  var errorText = [
    e.message,
    'URL: ' + e.filename,
    'Line: ' + e.lineno + ', Column: ' + e.colno,
    'Stack: ' + (e.error && e.error.stack || '(no stack trace)')
  ].join('\n');

  var client = new XMLHttpRequest();
  client.open('POST', 'https://your-error-tracker.example/log');
  client.setRequestHeader('Content-Type', 'text/plain;charset=UTF-8');
  client.send(errorText);
});

Filter the logs by the Googlebot user agent. The errors that appear only there are usually a blocked resource, a missing polyfill or a permission prompt.

4. Check at scale

One URL tells you about one template. To check thousands of pages in a single crawl, our guide to Screaming Frog custom JavaScript shows how to run your own checks against the rendered DOM, including whether an H1 is visible after render and how much content sits behind toggles.

The JavaScript SEO fixes that recover rankings

Everything below maps to a specific limitation in Google's rendering pipeline, and every one is documented in Google's own troubleshooting page.

1. Soft 404s in single-page apps

Your router shows a "not found" view but the server returned 200. Google indexes the empty page, Search Console reports a soft 404, and your error template can end up ranking for brand queries.

Google gives two options. Redirect to a URL where the server responds with a real 404:

fetch(`/api/products/${id}`)
  .then(res => res.json())
  .then(product => {
    if (!product.exists) {
      window.location.href = '/not-found';
    }
  });

Or add a noindex robots meta tag from JavaScript when the content does not exist:

fetch(`/api/products/${id}`)
  .then(res => res.json())
  .then(product => {
    if (!product.exists) {
      const metaRobots = document.createElement('meta');
      metaRobots.name = 'robots';
      metaRobots.content = 'noindex';
      document.head.appendChild(metaRobots);
    }
  });

The first option is better. It fixes the status code for every crawler, not just the one that renders.

2. Permission prompts

Google's guidance is to "expect Googlebot to decline user permission requests." If your content only appears after the user grants location, camera or notification access, Googlebot never sees it.

if (navigator.geolocation) {
  navigator.geolocation.getCurrentPosition(showLocalProducts, showDefaultProducts);
} else {
  showDefaultProducts();
}

function showDefaultProducts() {
  // Runs for Googlebot and for users who decline.
  // Put the content you need indexed here.
}

Store locators and delivery-area checks are the usual culprits.

3. Fragment URLs

Routing with example.com/#/products still shows up in audits. The AJAX crawling scheme has been deprecated since 2015, and Google's guidance is to use the History API instead:

// Instead of: window.location.hash = '/products'
history.pushState({ page: 'products' }, 'Products', '/products');

Every modern router does this by default. If yours does not, that is the migration to schedule first.

4. No state between page loads

The Web Rendering Service does not retain state. Local Storage, Session Storage and cookies are all cleared between page loads. Any logic like this fails silently for Googlebot:

// Breaks for Googlebot
if (localStorage.getItem('userAuthenticated')) {
  loadPremiumContent();
}

Render the public version of the content by default and layer the personalised version on top.

5. Content fingerprinting

Google says Googlebot "caches aggressively in order to reduce network requests and resource usage," and it may ignore your cache headers. The fix is to put a hash of the file contents in the filename:

// webpack, Vite and Next.js all do this by default
output: {
  filename: '[name].[contenthash].js'
}
// Produces main.2bb85551.js, which changes on every build

If you serve app.js with no fingerprint, Googlebot can keep running an old bundle long after you have shipped the fix.

6. Feature detection

Googlebot's Chromium is current, but headless rendering still differs from a user's browser. Google asks you to feature-detect every critical API and provide a fallback or polyfill:

if (window.WebGLRenderingContext) {
  renderWithWebGL();
} else {
  renderStaticFallback();
}

The failure mode is a bundle that throws before it reaches your content. One uncaught exception at the top of the app and nothing below it renders.

7. HTTP only

Googlebot uses HTTP requests and does not support WebSockets or WebRTC. If content arrives over a socket, add an HTTP fallback:

function initialiseDataStream() {
  if ('WebSocket' in window) {
    connectWebSocket();
  } else {
    setInterval(fetchDataViaHttp, 5000);
  }
}

Live pricing widgets and chat-style interfaces are where this bites.

8. Web components and shadow DOM

The Web Rendering Service flattens the light DOM and shadow DOM. Components that do not use the <slot> mechanism for light DOM content can lose that content in the rendered HTML. Check the rendered output in URL Inspection, and if content is missing, switch to slot-based projection or render the critical content on the server.

9. The noindex trap

Google says that when it encounters a noindex tag it "may skip rendering and JavaScript execution." So this never works:

// Too late: Google may not have rendered the page to run this
var metaRobots = document.querySelector('meta[name="robots"]');
metaRobots.setAttribute('content', 'index, follow');

Never ship noindex in the initial HTML if JavaScript is supposed to remove it. The same logic applies to canonicals and hreflang: JavaScript can add them, but a conflicting value in the source HTML wins the argument. That is a frequent cause of broken international SEO setups, and why we tell teams running hreflang across multilingual sites to emit those tags server-side.

10. Lazy loading and infinite scroll

Google's lazy loading guidance is explicit that "Google Search does not interact with your page." It does not scroll, click or hover. Anything that needs a user action to load never loads for Googlebot.

Use native loading="lazy" on images and iframes, or IntersectionObserver, and check that image URLs appear in the src attribute of the rendered HTML. For infinite scroll, give each chunk of content its own unique URL (for example ?page=12), link the pages sequentially, and update the URL with the History API as chunks load.

Anything that navigates through onclick, a javascript: href or a <span> with a router handler is invisible during the crawl phase. Crawl the site once with rendering off and compare the link counts against a rendered crawl; the difference is the set of pages Google can only discover after rendering, if at all.

Framework-specific advice for 2026

The frameworks have largely solved this, provided you use them the way they now ship.

Framework Default in the current major version What to check
Next.js (App Router) Layouts and pages are Server Components by default, rendered to HTML on the server Every 'use client' file you add pushes that content out of the initial HTML. Keep product text, headings and links in Server Components
Nuxt 4 Universal rendering (server-rendered, then hydrated) Route rules with ssr: false turn off server HTML for that route. Use prerender or isr for content pages instead
Angular SSR through @angular/ssr, with incremental hydration enabled by default when you use provideClientHydration() Content inside @defer blocks with hydrate never still renders on the server. Content loaded in ngOnInit from an API does not
Create React App, Vite SPA, Vue CLI SPA Client-side only The whole page depends on rendering. Migrate content routes to a server-rendering framework or a static build

The pattern across all three is the same. Server rendering is now the default and the SEO problems come from opting out of it, usually one component at a time, until the primary content of a template has quietly moved to the client. A quarterly crawl with rendering off is the cheapest way to catch that drift.

If you cannot move to server rendering this quarter, the order of fallbacks is static generation for the pages that earn traffic, then hybrid rendering (server-rendered content, client-side interactions). Dynamic rendering for bots is last, and Google now says so itself.

Verification

After a fix ships, check it in the order the crawlers will.

  1. Run Test live URL in URL Inspection and confirm the content, links and meta tags are in the rendered HTML and the console is clean.
  2. Curl the page as GPTBot and confirm the primary content is in the raw response.
  3. Request indexing for the affected templates and watch the Pages report over the following weeks.
  4. Keep the error logging in place. New releases reintroduce old problems.

Where this leaves you

JavaScript SEO in 2026 is two problems with one fix. Google will render what you ship, eventually, as long as you do not gate it behind a click, a permission or a stale bundle. AI crawlers will not render anything, so the content you care about has to be in the HTML response. Server rendering the primary content of every template satisfies both, and the modern versions of Next.js, Nuxt and Angular do it by default.

If you would rather have someone walk your stack template by template, that is what our technical SEO service and website audits are built for, and a free SEO review is a sensible place to start. Search is spreading across Google, ChatGPT and Perplexity, and the sites that render on the server are the only ones present in all of them.

More on technical seo
John Kyprianou

John Kyprianou

Founder & SEO Strategist

John brings over a decade of experience in SEO and digital marketing. With expertise in technical SEO, content strategy, and data analytics, he helps businesses achieve sustainable growth through search.

Keep reading

More to think about.

All insights
Diagram of a server access log with lines sorted into Googlebot, AI training crawlers and user-triggered retrieval bots, by SEO Turtle

Technical SEO

Log file analysis for SEO: what your server actually saw

A crawl tool tells you what a bot could do. A server log tells you what it did. Most log file guides are written for sites with millions of URLs, then handed to people with four hundred pages and no SSH access. Here is the honest version: why crawl budget is probably not your problem, why the bot population is, and where the logs actually live on Cloudflare, Vercel, cPanel and Shopify.

September 15, 2026
Diagram of a Google Search Console page indexing report with URLs sorted into technical faults and quality judgements, by SEO Turtle

Technical SEO

Crawled, currently not indexed: how to work out what is actually wrong

Google has looked at the page and decided not to keep it. Most advice treats that as a technical fault and sends you off to hammer Request Indexing. Here is the order that actually finds the cause: an afternoon of cheap plumbing checks, a look at what Google chose as the canonical, and then the harder question of whether the page deserves to exist.

September 8, 2026
Diagram showing Applebot's published IP pool growing from 2,400 to 7,056 addresses ahead of the Siri AI launch, by SEO Turtle

Technical SEO

Apple quietly tripled Applebot's crawl capacity weeks before Siri AI ships

Apple's published Applebot address pool went from 2,400 IPs to 7,056 with no blog post and no explanation, weeks before the rebuilt Siri ships in iOS 27. Most sites have never looked at how they treat Applebot, and a lot of them are blocking the wrong user agent. Here is what changed and what to check this week.

August 27, 2026
Diagram of ChatGPT's three retrieval layers, its own index, a shared read cache and rare live page opens, by SEO Turtle

Technical SEO

ChatGPT almost never opens your page. Here is what it reads instead

New research pulled apart how ChatGPT actually fetches web pages, and the answer is uncomfortable. It runs its own index that barely overlaps with Bing, serves most answers from a cached copy of your page, and only truly opens about one page in eighty. Here is what that means for how you write and structure a page.

August 18, 2026
Illustration of a stack of duplicate web pages marked with a red cross resolving into one clean authoritative page that feeds an AI answer, by SEO Turtle

Technical SEO

Most AI visibility wins are just technical debt you finally paid off

Businesses are buying GEO tools to fix problems a 2019 site migration created. AI search did not add new technical requirements, it just stopped compensating for the old ones. Here is our practitioner take on why the technical SEO backlog is now the AI visibility roadmap, and why that favours smaller sites.

August 17, 2026