Why AI Crawlers Cannot See Your Animated Hero
- seo
- geo
- nextjs
- react
If your page content only appears after JavaScript runs, AI search crawlers will not see it. Unlike Googlebot, which maintains a second rendering pass, most LLM-facing crawlers fetch the raw HTML and read what is there. Whatever arrives empty stays empty.
The failure is silent
This is the part that makes it dangerous. A client-rendered hero looks perfect in
your browser, ranks acceptably in Google because Googlebot eventually renders it,
and returns an empty <h1> to every AI crawler that asks. Nothing errors.
Nothing warns. The page simply stops being quotable.
The usual cause is an animation wrapper. You reach for a nice text-reveal
component, pass your headline in as a prop, and that headline now exists only
inside a "use client" boundary.
How to check in ten seconds
Curl the page and look for your headline:
curl -s https://example.com | grep -o '<h1[^>]*>.*</h1>'If that returns nothing, the content is client-only. view-source: in a browser
shows the same thing — the raw document before any script has run.
The part everyone gets wrong
The intuitive diagnosis is "text inside a client component is missing from the HTML." That is not true, and believing it sends you chasing the wrong fix.
In the Next.js App Router, Client Components are server-rendered too. A
"use client" component is rendered to HTML on the server and merely hydrated
in the browser. Passing your headline into one as a prop does not, on its own,
remove it from anything.
Three patterns actually lose content:
// 1. Effect-gated render — the server emits nothing at all.
const [mounted, setMounted] = useState(false);
useEffect(() => setMounted(true), []);
if (!mounted) return null;
// 2. Explicitly opting out of server rendering.
const Hero = dynamic(() => import("./hero"), { ssr: false });
// 3. Text present in the HTML, but painted invisible until JS animates it.
<motion.h1 initial={{ opacity: 0 }} animate={{ opacity: 1 }}>
{title}
</motion.h1>;The first two are what cost you AI citations. The third still delivers the text to a crawler, but leaves a human staring at a blank page if your JavaScript fails to load — worth fixing for a different reason.
CSS-only animations avoid all three. A @keyframes fade-in runs without
JavaScript, so the content is both present and visible.
Make it a test, not a habit
Habits decay; tests do not. Playwright can load your pages with JavaScript disabled and assert the content is both present and visible:
test.use({ javaScriptEnabled: false });
test("headline survives without JavaScript", async ({ page }) => {
await page.goto("/");
const heading = page.locator("h1");
await expect(heading).toHaveCount(1);
// Playwright counts opacity-0 elements as visible, so check it directly.
const opacity = await heading.evaluate((el) => getComputedStyle(el).opacity);
expect(Number(opacity)).toBeGreaterThan(0);
});Now the next person to gate a headline behind mount state gets a red build instead of a quiet ranking decline six weeks later.