agentspeed.
← all fixes
Issue remediation

Primary content requires JavaScript

Your main content is rendered by JavaScript, so most agents see an empty page.

failed checkcontent.js_required_for_primary_content
What this means

An agent fetches your URL the way curl does: one HTTP request, no browser, no JS engine. If the response body is mostly empty divs that get filled in by client-side React, Vue, or similar, the agent reads the empty version. Your headline, body copy, prices, and structured data are invisible. This is the single most common reason a real, working website scores poorly on agent readiness.

Why it matters for AI agents

Most AI crawlers (GPTBot, ClaudeBot, PerplexityBot, OAI-SearchBot) do not run JavaScript. They fetch the raw HTML and parse what's there. Even agents that can render JS (Googlebot, some advanced crawlers) treat JS-rendered pages with skepticism; they're slower, costlier, and more likely to fail. A page where the H1 is in the rendered HTML wins over a page where the H1 only appears after hydration.

Business impact

A SPA homepage that returns <div id="root"></div> as its only content is functionally invisible to agent answer engines. Migrating to SSR or SSG typically takes a sprint and pays back permanently: agent citations appear, structured data starts working, and SEO improves as a side-effect. The opportunity cost of waiting is every agent query that picks a competitor with a server-rendered page.

How to check manually

View source (Ctrl-U, not Inspect Element, which shows the post-render DOM). Search for your headline. If you don't find it, your content is JS-dependent. Or run `curl -s https://your-domain.com | grep -i 'your headline'` from a terminal. If grep finds nothing, agents see nothing.

The exact fix

Render the primary content on the server. The choice depends on your stack: server-side rendering (SSR), static-site generation (SSG), or incremental static regeneration (ISR). All three deliver the same fix: the HTTP response contains the actual H1, body text, and structured data. Client-side hydration can still happen for interactivity; the rule is just that the meaningful content is in the initial HTML.

Copy-paste snippet
Reference snippethtmlrendered-html.html
<!-- Server-rendered: agent reads this directly. -->
<!DOCTYPE html>
<html>
<head>
  <title>Acme — Cookware made in Ohio</title>
  <meta name="description" content="Pre-seasoned cast-iron skillets, made in the USA.">
</head>
<body>
  <h1>Cookware that lasts a lifetime.</h1>
  <p>Hand-poured cast iron, finished in Ohio. Free shipping over $75.</p>
  <a href="/products">Shop the catalogue</a>
  <!-- Client-side React hydrates here for interactivity, but the agent
       has already read the meaningful content above. -->
</body>
</html>
Platform-specific implementations

The reference snippet above is platform-agnostic. Below are the concrete steps for the four most common stacks plus a generic fallback for any other host.

Next.js

If you're already on the App Router, server components are the default, so content rendered in a page.tsx without 'use client' ships in the initial HTML. The common failure is wrapping the whole page in 'use client' for one interactive button. Instead, keep the page server-rendered and isolate the interactive bit into a small client component imported at the leaf. For Pages Router, use getStaticProps or getServerSideProps; do not pull data inside useEffect for above-the-fold content.

Next.jstsxapp/page.tsx
// Server component by default — content lands in initial HTML.
import { ProductCarousel } from './ProductCarousel'; // client component
import { getFeaturedProducts } from '@/lib/products';

export default async function Home() {
  const products = await getFeaturedProducts();
  return (
    <main>
      <h1>Cookware that lasts a lifetime.</h1>
      <p>Hand-poured cast iron, finished in Ohio.</p>

      {/* Interactive island — only this subtree ships JS. */}
      <ProductCarousel products={products} />

      <h2>Why cast iron</h2>
      <p>Heat retention, even browning, no PFAS coatings. Read more →</p>
    </main>
  );
}

WordPress

WordPress is server-rendered by default; themes output HTML on the server. JS-dependent content typically creeps in via page builders (Elementor, Divi) that defer rendering, or via React-based block themes. Audit your theme: if you use a builder, switch off "lazy load" / "dynamic render" for above-the-fold sections. If you use a headless setup, switch the front-end framework to SSR/SSG (Next.js, Astro, Eleventy).

WordPressphppage-template.php
<?php /* Template Name: Server-rendered landing */ ?>
<?php get_header(); ?>

<main>
  <h1><?php the_title(); ?></h1>
  <?php the_content(); ?>
  <?php // Interactive embed (newsletter form, carousel) goes after the
        // primary content so agents see the meaningful HTML first. ?>
  <?php echo do_shortcode( '[newsletter_signup]' ); ?>
</main>

<?php get_footer(); ?>

Shopify

Liquid templates are server-rendered, and Shopify storefronts ship HTML. The trap is replacing sections with React/Hydrogen apps that render on the client. If you're on standard Shopify, your product pages are already in good shape; verify by view-source on a product URL. If you're on Hydrogen (Shopify's React stack), make sure SSR is enabled (the default) and don't move primary content into useEffect.

Shopifyliquidsections/main-product.liquid
{% comment %}
  Server-renders product title, description, price into the initial HTML.
  Agents fetching the page see all of this without running JS.
{% endcomment %}
<article>
  <h1>{{ product.title }}</h1>
  <p>{{ product.description | strip_html }}</p>
  <p class="price">{{ product.price | money }}</p>
  {% render 'product-form', product: product %}
</article>

Webflow

Webflow exports static HTML at publish time, so content authored in the Designer ships in the initial response. The fix is mostly about making sure dynamic CMS content uses Collection Lists (server-rendered) rather than custom JS that fetches at runtime. If you have third-party embeds (Typeform, Calendly), keep them below the fold so above-the-fold content reads instantly.

WebflowhtmlDesigner → CMS Collection list
<!--
  Webflow renders Collection Lists at publish time, so the resulting
  HTML contains every item. Avoid custom <script> blocks that fetch
  CMS items at runtime — those leave agents looking at an empty list.
-->
<div class="collection-list-wrapper">
  <!-- Each item bound to a Collection field is in the initial HTML. -->
  <h2>{{ Article Title }}</h2>
  <p>{{ Article Excerpt }}</p>
</div>

Static HTML / any stack

If you control the build, generate HTML at build time. Static-site generators (Astro, Eleventy, Hugo, Jekyll, Next.js export) all produce ready-to-serve HTML. Avoid hand-rolled SPAs (create-react-app, Vite + React without SSR) for content sites; they put your H1 in JavaScript. If you must use a SPA framework, pick one with SSR support (Next.js, Nuxt, SvelteKit, Remix, Astro).

Static HTML / any stackastrosrc/pages/index.astro
---
// Astro components run at build time; the rendered HTML is what
// the agent fetches.
import Layout from '../layouts/Layout.astro';
import { getFeaturedProducts } from '../lib/products';

const products = await getFeaturedProducts();
---

<Layout title="Acme — Cookware made in Ohio">
  <h1>Cookware that lasts a lifetime.</h1>
  <p>Hand-poured cast iron, finished in Ohio.</p>

  <ul>
    {products.map((p) => (
      <li><a href={`/products/${p.slug}`}>{p.name} — ${p.price}</a></li>
    ))}
  </ul>
</Layout>
Common mistakes
How to re-test

After deploying SSR/SSG, run `curl -s https://your-domain.com | grep -i '<h1'` and confirm your real headline appears. Open view-source in a browser and confirm structured data, body copy, and links are all in the source. Run a fresh AgentSpeed scan, and the 'content js required for primary content' check should pass; the readability score will tick up.

FAQ
Will SSR slow down my page?

No. SSR adds a small server-side latency cost (typically 30–80ms) but reduces time-to-interactive because the browser has content to paint immediately. Lighthouse and Web Vitals improve, not regress, after a SPA → SSR migration.

Can I keep my React app and just add SSR?

Yes. Next.js, Remix, Astro, and Vite SSR all let you reuse your React components on the server. The migration is mostly tooling: replace create-react-app with one of those frameworks, then move data fetching out of useEffect.

What about prerendering services like Prerender.io?

They work as a stopgap but add latency, cost, and a separate cache to maintain. They also do not always serve the prerendered version to all crawlers (user-agent detection is fragile). Native SSR/SSG is the durable fix.

Verify on your site
Run a scan

Free scan, no signup. The score page is permanent and citable, so you can link it from your team's remediation ticket alongside this guide for “Primary content requires JavaScript”.

Run a free scan →See all fixes →