/ โ† Agent Readiness Fixes / React and Next.js
In this guide
  1. Why React sites are often invisible to AI crawlers
  2. What a crawler receives from each setup
  3. Next.js specifics that trip people up
  4. Routing, links and status codes in single-page apps
  5. The fix, step by step
  6. The rest of the audit on a React site
  7. Keeping it right as the app grows
  8. Questions and answers

Why React sites are often invisible to AI crawlers

A classic React single-page app sends the same tiny HTML file for every URL: a head, an empty <div id="root"></div> and a script bundle. The browser downloads the bundle, React starts, works out which page to show from the address, fetches the data and builds the page. Everything a visitor reads is created in those last steps.

Google renders JavaScript in a second pass, so client-rendered React pages can still appear in Google Search. Most AI crawlers do not do this. They read the HTML from the first response and extract text from it. For a client-rendered app, that text is often just the page title and a "You need to enable JavaScript" message.

Server rendering frameworks fix this in principle, because the server runs your components and sends real HTML. In practice, several React habits undo it:

๐Ÿ’ก The React team deprecated Create React App in early 2025 and now points new projects to frameworks with server rendering or static generation built in. If your site was started with it, a move to a framework is the lasting fix rather than a workaround.

What a crawler receives from each setup

SetupHTML a crawler receivesUsual fix
Create React App, or Vite with the plain React templateAn empty root div and scriptsAdd build-time pre-rendering for public routes, or move to a framework
Next.js App Router, server components fetching dataComplete HTMLNothing, as long as content is not moved into browser-only code
Next.js with data fetched in useEffect or a client-side data libraryThe layout and a loading stateFetch in a server component, or pass server-fetched data in as props
Next.js Pages Router with getStaticProps or getServerSidePropsComplete HTMLNothing; check no page relies on client-only fetching instead
React Router in framework mode, or RemixComplete HTML when loaders fetch the dataMove fetching into loaders; pre-render static routes
Gatsby, or Astro with React componentsComplete HTML built at deploy timeCheck components marked to render only in the browser

The label on the box matters less than what arrives. Test the actual HTML response for each template, as described below, before deciding what to change.

Next.js specifics that trip people up

The use client boundary

In the App Router, components are server components unless a file starts with 'use client'. Client components are still rendered to HTML on the server for the first response, so marking a component as a client component does not hide its markup by itself. What hides content is fetching the data after the component mounts. This pattern leaves crawlers with the loading message:

'use client'
export default function Reviews({ productId }) {
  const [reviews, setReviews] = useState(null)
  useEffect(() => {
    fetch(`/api/reviews/${productId}`).then(r => r.json()).then(setReviews)
  }, [productId])
  if (!reviews) return <p>Loading reviews...</p>
  return <ReviewList items={reviews} />
}

The same content fetched in a server component arrives complete, and any interactive part can still be a small client component inside it:

export default async function Reviews({ productId }) {
  const reviews = await getReviews(productId)
  return <ReviewList items={reviews} />
}

Components loaded without server rendering

dynamic(() => import('./Specs'), { ssr: false }) tells Next.js to leave that component out of the server HTML. It is meant for things that genuinely cannot run on a server, such as a map library that needs window. Search your codebase for ssr: false and check none of those components hold text that matters.

Streaming and loading states

With loading.tsx or Suspense boundaries, Next.js sends the page frame first and streams slower parts later in the same response. The content is still in the HTML, but it comes at the end of the document. Keep the core text outside slow Suspense boundaries so it arrives in the first part of the response, and keep slow calls such as recommendations inside them.

Metadata and structured data

Use the Metadata API for titles, descriptions, canonical URLs and social tags, so they are part of the server response. Set metadataBase in the root layout so relative canonical paths become full URLs. JSON-LD is not part of the Metadata API; render it as a script tag in the page component, escaping the less-than character as the Next.js documentation recommends:

export default async function Page({ params }) {
  const { slug } = await params
  const guide = await getGuide(slug)
  const jsonLd = {
    '@context': 'https://schema.org',
    '@type': 'Article',
    headline: guide.title,
    dateModified: guide.updatedAt,
  }
  return (
    <article>
      <script type="application/ld+json"
        dangerouslySetInnerHTML={{ __html: JSON.stringify(jsonLd).replace(/</g, '\\u003c') }} />
      <h1>{guide.title}</h1>
      ...
    </article>
  )
}

Next.js also supports app/robots.ts and app/sitemap.ts, which generate those files at build or request time. If you use them, check the output at /robots.txt after each deploy, because a rule written for a staging environment can easily reach production.

Routing, links and status codes in single-page apps

Even with rendering fixed, single-page apps have three routing habits that confuse crawlers.

If you pre-render a Vite or similar app at build time rather than migrating, make sure every public route is in the pre-render list, including product and article pages generated from data. Routes left out fall back to the empty shell.

The fix, step by step

Step 1

Find out which rendering mode each route uses

List your templates: home, listing, detail, article, landing pages. For each one, fetch a live URL with curl using a crawler user agent, save the HTML and search it for a sentence from the main content. In Next.js, the build output also marks each route as static or dynamic. Record which routes arrive complete, which arrive as a layout with loading states and which arrive as an empty root element.

Step 2

Move data fetching to the server

For every piece of public content fetched after mount, move the fetch to where the framework runs it on the server: an async server component in the Next.js App Router, getStaticProps or getServerSideProps in the Pages Router, or a route loader in React Router framework mode. Prefer static generation with timed revalidation for content that changes daily or less, so the finished HTML can be cached.

Step 3

Keep client components for interaction only

Go through files marked 'use client' and components loaded with ssr: false. Where one holds content as well as interaction, split it: render the text, prices and specifications in a server component and wrap only the interactive control, such as a quantity picker or filter panel, in a small client component. Remove any rendering that depends on window or localStorage for public content.

Step 4

Give every page a real URL and a real link

Replace hash-based routes with path-based ones and make sure the host serves each path directly. Search the code for click handlers that call navigate or router.push on elements that are not links, and replace them with the router's link component so each renders an anchor with an href. Tabs or filters that show different content should each have their own address if that content should be found.

Step 5

Output metadata and structured data from the server

Move titles, descriptions, canonical URLs and social tags out of browser-side head managers and into the Metadata API or your framework's equivalent, so they appear in the first response. Set metadataBase once in the root layout. Render JSON-LD as a script tag inside the server-rendered page, built from the same data as the visible content, so the two cannot drift apart after a content edit.

Step 6

Return proper status codes

Request a URL that does not exist and check the status code with curl -I. If it returns 200, fix it: call notFound() in Next.js when a record is missing, throw a 404 response from a React Router loader, or configure your static host to serve a 404 page with a 404 status. Use real server redirects with a 301 or 308 status for moved pages rather than redirecting in the browser.

Step 7

Build it into your tests

Add an automated test that fetches a sample URL for each template from a production build, without running scripts, and fails if a known phrase, the h1 or the JSON-LD block is missing. Run it in your deployment pipeline so a future change that moves content into the browser is caught before release. Then run the Agent Readiness Audit on each template and keep the results for comparison.

The rest of the audit on a React site

Rendering is the React-specific part. The other findings are fixed the same way on any stack, and each has its own guide:

Speed matters too once the HTML is complete: a server-rendered page with slow data calls and no caching can still be too slow for a crawler. See fixing slow rendering for AI crawlers.

Keeping it right as the app grows

  1. Agree a team rule: public content is fetched on the server; browser-side fetching is for personal or interactive data only.
  2. Flag new uses of ssr: false and new 'use client' files in code review and ask what they render.
  3. Keep the raw-HTML test from step 7 in the pipeline, and extend it when you add a template.
  4. After framework upgrades, re-check route rendering modes, because defaults for caching and static generation have changed between major Next.js versions.
  5. Re-run the Agent Readiness Audit after hosting, CDN or framework changes.
Questions and answers

1. Google indexes my React app, so why would AI crawlers not see it?

Google runs a rendering service that executes JavaScript, so it can see pages that React builds in the browser, although sometimes with a delay. Most AI crawlers do not render scripts; they take the HTML from the first response and extract text from that. A client-rendered app therefore works for Google and fails for them. Check the raw HTML with curl or with JavaScript disabled, because Google tools will not show you this difference.

2. Does marking a component use client hide it from crawlers?

Not by itself. In the Next.js App Router, client components are still rendered to HTML on the server for the first request, so their initial markup is in the response. The problem comes from what they do after that: fetching data in useEffect, reading browser storage or waiting for user interaction before showing content. If the text depends on any of those, the server HTML contains the loading state instead of the content, and that is what the crawler keeps.

3. Do I need to move from a Vite single-page app to Next.js?

Not necessarily. If only a handful of routes are public and their content changes rarely, pre-rendering those routes at build time can be enough, and several tools and plugins do this for Vite projects. If you have many data-driven public pages such as products or articles, a framework with server rendering or static generation, whether Next.js, React Router in framework mode or Astro, is usually less work to maintain than keeping a pre-render list up to date.

4. Is a pre-rendering service a good fix for a React app?

It can be a practical stopgap for an older app that cannot be rebuilt soon. The service runs your pages in a headless browser, stores the resulting HTML and serves it to crawlers. The drawbacks are cost, snapshots that go out of date, and the risk of bots and people seeing different content, which search engines treat as cloaking. Google describes serving bots a separate rendered version as a workaround, so plan a proper rendering change as well.

5. How do I add JSON-LD in the Next.js App Router?

Build the structured data object in the page component from the same data used for the visible content, then render it as a script tag with type application/ld+json and the object serialised with JSON.stringify. The Next.js documentation recommends escaping the less-than character in that string to avoid injection problems. Put site-wide types such as Organization in the root layout and page-specific types such as Article or Product in the page itself.

6. Why does my Next.js site show a loading spinner in the raw HTML?

Either the content is fetched in browser-only code, or it sits inside a Suspense boundary or loading file and is streamed later in the response. In the first case, move the fetch into a server component or a server data function. In the second, the text is still in the response but near the end, after the spinner markup. Keep the core content outside slow Suspense boundaries so it arrives in the first part of the document.

7. Can AI crawlers follow links in a React app?

Only if they are real links. Crawlers look for anchor elements with an href attribute and request those addresses. Router link components in Next.js and React Router render proper anchors, so they work. Buttons, divs or spans with click handlers that change the route do not, and neither do hash-based addresses, which all look like the same page. If a crawler cannot discover a page through links or your sitemap, it will rarely fetch it at all.

Check what AI crawlers see from your React pages

The Agent Readiness Audit requests your page with real AI crawler user agents and tests how much of the content depends on JavaScript, alongside robots.txt, llms.txt, JSON-LD, semantic HTML and content extractability checks.

Open Agent Readiness Fixes โ†’
๐Ÿ’ฌ Got a problem?
Related Guides: All Agent Readiness Fixes  ·  Fixing Slow Rendering for AI Crawlers  ·  Fixing Non-Semantic HTML Structure  ·  AI Agent Compatibility for Single-Page Apps  ·  Learning Hub

About aiwebpageseo

aiwebpageseo.com is a data-driven SEO and AEO (Answer Engine Optimisation) platform providing a free suite of technical SEO tools. Rather than relying on AI-theorised assumptions, the platform analyses live URL performance, delivering objective diagnostics, page speed metrics, CLS debugging, and site crawl data alongside actionable technical tutorials.