Is my Next.js site visible to ChatGPT?
The short version
Next.js is one of the better frameworks for AI visibility, and the common worry — "my app is React, so assistants cannot see it" — is usually wrong. The App Router renders Server Components to HTML on the server, and static routes are prerendered at build time. A crawler that does not execute JavaScript still gets your content.
The failure, when it happens, is narrow and specific. It is not "React" and it is not "client components". It is a handful of patterns that defer content until after hydration, and they are worth knowing precisely, because guessing leads people to rewrite pages that were never broken.
The myth: "use client" does not make a component invisible
This is the single most common misconception, and it causes real wasted work. A "use client" directive marks a component as interactive — it hydrates in the browser and can use state and effects. It does not opt out of server rendering. Next.js still renders that component to HTML on the server for the initial response.
We verified this against a real Next.js 16.2.12 production build (next build && next start), requesting each route with the OAI-SearchBot user-agent and grepping the raw response for a unique marker string. A client component with static content came back present in the HTML.
| Pattern | In the raw HTML? |
|---|---|
| Server Component (default) | Present |
| 'use client' component, static content | Present |
| 'use client' + content set in useEffect | Absent |
| dynamic(() => import(...), { ssr: false }) | Absent |
| Mounted guard: if (!mounted) return null | Absent |
The four patterns that actually break it
Content set in useEffect. The effect only runs in the browser, so the server renders the empty initial state. A headline held in state that useEffect fills in arrives as an empty element.
dynamic() with ssr:false. This tells Next.js explicitly not to render the component on the server. The crawler gets your loading placeholder and nothing else.
The mounted guard. The pattern of returning null until a useEffect sets mounted to true — commonly copied in to silence hydration mismatch warnings — makes the server render literally nothing for that subtree.
Pages Router client-side fetching. getServerSideProps and getStaticProps both put data in the HTML. Fetching in useEffect instead does not, and that is the usual Pages Router version of the same mistake.
The exact signature to grep for
Next.js leaves a specific string in the HTML when a subtree bails out to client-side rendering. If this appears in your response, something on that route is set to ssr:false and a crawler is not getting it. This is a definitive answer, not an inference.
curl -s https://yoursite.com | grep -c "BAILOUT_TO_CLIENT_SIDE_RENDERING" # any count above 0 means part of that page never rendered on the server
What the broken output actually looks like
These are the real response bodies from the reproduction above, with script tags stripped. Each one is a page that looks completely normal in a browser.
The ssr:false case is the most recognisable — your loading state is the entire document, next to the bailout template. The useEffect case leaves an empty heading, which is easy to miss because the tag is there and only the text is gone. The mounted guard leaves nothing at all.
<!-- dynamic(..., { ssr: false }) -->
<template data-dgst="BAILOUT_TO_CLIENT_SIDE_RENDERING"></template><p>Loading…</p>
<!-- content set in useEffect -->
<main><h1></h1></main>
<!-- if (!mounted) return null -->
<!--$--><!--/$-->Check your own route in one command
Count the visible words in the raw response and compare that against what you see in the browser. A large gap means content is arriving only after hydration.
curl -s -A "Mozilla/5.0 AppleWebKit/537.36 (KHTML, like Gecko); compatible; OAI-SearchBot/1.0; +https://openai.com/searchbot" \ https://yoursite.com | sed 's/<[^>]*>/ /g' | wc -w
How to fix each one
For useEffect data: move the fetch into a Server Component and await it, or into getServerSideProps on the Pages Router. The content then exists before the response is sent.
For ssr:false: remove the flag if the component only needed lazy loading rather than browser-only APIs. If it genuinely touches window or a browser-only library, keep ssr:false but server-render the text content around it, so the crawler gets the substance and only the widget is deferred.
For the mounted guard: this pattern is almost always the wrong fix for a hydration mismatch. Address the actual mismatch — usually a date, a random value or a localStorage read — and render the content unconditionally.
Then re-check with curl. "Next.js is SSR" is a property of the framework, not a guarantee about a given route.
Two things that are not about rendering at all
Your rendering can be perfect and you can still be unreachable. Next.js generates robots.txt from app/robots.js, and a blanket Disallow there blocks retrieval crawlers no matter how good your HTML is. Check what is actually served at /robots.txt rather than reading the source.
Separately, edge bot-management and firewall rules — including on Vercel — reject crawler requests before your application runs. A 403 to OAI-SearchBot is invisible in your analytics because the request never reaches your app.
curl -s https://yoursite.com/robots.txt
curl -s -o /dev/null -w "%{http_code}\n" \
-A "Mozilla/5.0 AppleWebKit/537.36 (KHTML, like Gecko); compatible; OAI-SearchBot/1.0; +https://openai.com/searchbot" \
https://yoursite.comWhere this evidence stops
The reproduction above was run against Next.js 16.2.12. These are stable, documented behaviours rather than version quirks, but React and Next.js rendering internals do change, and the exact bailout string is an implementation detail that could be renamed in a future release. The word-count and status-code checks do not depend on it.
It also tells you what a crawler can fetch, which is not the same as what an assistant will choose to cite. Nobody can measure citation reliably at scale, and we do not claim to. What is measurable is whether your content is reachable and present — and that is a prerequisite, so it is worth getting right first.
Frequently asked
Does "use client" hide my content from ChatGPT?
No. Client components are still server-rendered to HTML for the initial response. Content only disappears if it is set in useEffect, guarded behind a mounted check, or loaded via dynamic() with ssr:false.
Is the App Router better than the Pages Router for AI crawlers?
Both can produce fully server-rendered HTML. The App Router server-renders by default, whereas the Pages Router depends on whether you use getServerSideProps or getStaticProps rather than fetching in useEffect.
Does the Next.js Image component affect AI readability?
No. It renders a real img tag with your alt text in the HTML. Alt text is worth writing well, since it is often the only description of an image a crawler receives.
Do I need llms.txt for a Next.js site?
No major vendor has committed to reading llms.txt, so it should come after crawler access and rendering, not before. It is cheap to add once those are sorted.
Why does my site rank in Google but not appear in ChatGPT?
Googlebot renders JavaScript; AI crawlers do not. A route whose content arrives after hydration can rank normally in Google while being blank to ChatGPT, Claude and Perplexity.
Last updated 2026-07-26.
Check your own site
Runs every check in this guide against fourteen AI crawlers. Free, no account.
No signup. No credit card. Results are a shareable link.