Trakkr Docs

JavaScript rendering and AI crawlers

Your site looks fine in a browser. The hero animates in, the pricing table fills with numbers, the FAQ opens on click. Then GPTBot fetches the same URL and walks away with a near-empty document.

That is the risk with client-rendered content. Browsers run scripts; most AI crawlers use the first HTML response as-is. If your useful text only exists after a React or Vue app boots, a crawler receives a shell. This guide covers how to tell whether that is happening to you, and what to change.

One important exception: Gemini renders JavaScript, because Google's rendering service handles the fetch. ChatGPT, Claude and Perplexity do not. So a client-rendered page is not invisible everywhere, just in most of the places that matter.

What the crawler receives

A browser downloads the HTML, executes the JavaScript, and paints the result. A non-rendering crawler stops after step one. Whatever the server put in that first response is the whole page as far as it is concerned.

That makes the test simple. Open your page, disable JavaScript, and reload. What you can still read is roughly what a non-rendering crawler gets. Or fetch it the way a crawler would:

curl -sL https://your-site.com/ | head -100

If your headline, product description and key facts are not in that output, they are not reaching the models that do not render.

How Optimize measures it

Optimize reports this in two places, answering two different questions.

The JS Dependency check

A fast read of the initial HTML, graded on the words that survive a plain fetch with no JavaScript run. It looks for framework fingerprints and for the classic empty-shell signature: a mount node such as #root, #app or #__next that contains almost nothing, alongside at least one script tag.

What the fetch containsResult
An empty app shell and under 40 visible wordsFail
A framework detected and under 60 words of primary contentFail
A framework detected and under 200 words of primary contentWarning
No framework fingerprint, under 40 visible words, and either three or more scripts or a large HTML payloadFail
Anything elsePass

The check counts words rather than bytes on purpose. A 300KB JavaScript bundle does not make a page readable.

What bots receive

The page drawer on the Pages tab runs two live fetches: one with the exact GPTBot user-agent, one with a JavaScript-rendered browser baseline. It then chunks both by heading and diffs them, so you get the browser's word count next to the bot's, a list of which sections survived and which were lost, and a sentence naming which platforms can see the page.

A section counts as lost when its bot version keeps under 30% of the browser version's words. When no rendered baseline is available, the section shows what bots receive and says the comparison is unavailable rather than inventing a diff.

At site level, Optimize also states a single verdict as a fact on the Findings tab: Pre-rendered for bots, Partly rendered for bots or Empty shell for bots. It comes from the ratio of visible characters in the bot fetch to the browser fetch, with an absolute floor: under 200 visible characters, or under 15% of the browser's text, reads as an empty shell; 15% to 60% is partly rendered; 60% or more is pre-rendered.

Symptoms worth checking

Fixes, in order of leverage

Server-side rendering

SSR builds the HTML on the server and sends it complete. Your framework still hydrates in the browser; the crawler just gets a usable document first.

Next.js server-renders by default in the App Router. In the Pages Router, any page without getStaticProps is server-rendered on request:

// app/about/page.tsx - a server component, rendered on the server
export default function About() {
  return <h1>About Nike</h1>
}

Nuxt has SSR on by default:

// nuxt.config.ts
export default defineNuxtConfig({
  ssr: true,
})

Static generation

SSG builds each page at deploy time, so every request is served a finished HTML file. It is the strongest option for content that does not change per visitor.

// pages/about.js - built once at deploy time
export async function getStaticProps() {
  return { props: { content: 'About Nike' } }
}

Gatsby generates static HTML by default and needs nothing extra.

Prerendering, when you cannot migrate

For a React app with no SSR framework, react-snap or a prerendering service will produce static HTML for your routes. It is a patch rather than a fix, but it puts real text in the first response.

Angular's equivalent is server-side rendering, added with ng add @angular/ssr.

Mix deliberately

Most frameworks let you choose per route. Server-render or statically generate the pages you want AI to quote, and leave genuinely interactive surfaces client-side. Dashboards and account pages are fine as SPAs; nobody needs a model to read them.

What to prioritise

Do not rewrite the whole site. Server-render the pages that answer questions people ask AI about you: your home page, product and service pages, pricing, and the guides or comparisons you want cited. Pages ranks your URLs by journey stage, and Crawlers shows which ones bots keep returning to. Between them you get an ordered list rather than a guess.

Common mistakes

Verify the fix

View source on the changed page and confirm the important text is in the raw HTML. Run a new Optimize check and read the JS Dependency result and the site verdict. Then open the page drawer and look at what bots receive: the browser word count and the bot word count should be close.

Crawler activity is the slower confirmation. Once a page is pre-rendered, watch Crawlers for indexing and conversation fetches on it. You cannot force an operator to recrawl, so give it days rather than minutes.

Common questions

Do all AI crawlers ignore JavaScript?

Not all. Gemini renders JavaScript because Google's rendering service does the fetch. ChatGPT, Claude and Perplexity do not, so a client-rendered page reaches Gemini and effectively disappears for the rest.

Does a passing JS Dependency check mean the page is fine?

It means enough text survived a plain fetch to clear the threshold. The deeper comparison in the page drawer is the one that shows which specific sections are lost, and a page can pass the fast check while still dropping an FAQ or a spec table.

Will fixing this also help my Google rankings?

Usually, yes. Google renders JavaScript but renders it later and on its own schedule, so server-rendered content is indexed sooner and more reliably. The same change helps both, which is a reason to do it once rather than build a bot-only path.

Should I serve different HTML to crawlers?

No. Serving crawlers different content from browsers is cloaking, and it puts your search visibility at risk. Server-render the real page instead. If you want a machine-readable companion to the real page, AI Pages is the supported way to do it.

How long until Trakkr shows the improvement?

The Optimize checks update on the next scan you run, so those are immediate. Crawler evidence depends on operators revisiting the page, which happens on their timetable, not yours.