Skip to content
AS

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.