Skip to main content
Sometimes Hado SEO pre-renders your page before content has loaded, resulting in 404 pages, blank content, or loading states being cached.

Symptoms

  • Google shows your 404 page in search results
  • Pre-rendered HTML contains “Loading…” or spinner
  • Content is blank or incomplete

How the Renderer Decides Your Page Is Ready

The renderer loads your page in a headless browser and runs through these checks:
  1. Network idle — waits up to 5 seconds for all network requests to finish
  2. Loading indicator check — looks for visible spinners or loading text
  3. Content thresholds — requires at least 100 characters of visible text and 20 DOM elements
If your page doesn’t pass these checks within 12 seconds, the renderer captures whatever is on screen at that point — which may be a loading state, an error, or blank content.

What counts as a loading indicator?

The renderer detects these CSS selectors:
.loading, .spinner, [data-loading], .skeleton, [aria-busy="true"]
It also detects loading text like “Loading…”, “Please wait”, “Cargando”, “Chargement”, and equivalents in 20+ languages — but only when the page has less than 200 characters of visible text.
If your app uses none of these indicators while fetching data, the renderer has no way to know it should keep waiting. It may capture a half-rendered page that technically passes the content thresholds.

Common Causes and Fixes

Cause: Your app fetches data after mount but doesn’t show any loading state. The renderer sees the initial shell (which may have enough DOM elements and text to pass thresholds) and captures it before data arrives.Fix: Add a recognized loading indicator while data is being fetched:
// Bad - renders partial content with no loading signal
function ProductPage() {
  const [product, setProduct] = useState(null);
  useEffect(() => { fetchProduct().then(setProduct); }, []);
  return <div>{product ? <ProductDetails /> : <EmptyShell />}</div>;
}

// Good - shows a loading indicator the renderer will detect
function ProductPage() {
  const [product, setProduct] = useState(null);
  useEffect(() => { fetchProduct().then(setProduct); }, []);
  if (!product) return <div className="loading">Loading...</div>;
  return <ProductDetails product={product} />;
}
Use any of these recognized patterns: .loading, .spinner, .skeleton, [data-loading], or [aria-busy="true"].
Cause: Your app uses custom class names like .is-loading, .page-spinner, or [data-state="loading"] that the renderer doesn’t recognize.Fix: Switch to one of the recognized selectors, or add a recognized class alongside your custom one:
// Bad - custom class not detected
<div className="page-spinner">Loading...</div>

// Good - add a recognized class
<div className="page-spinner loading">Loading...</div>

// Also good - use aria-busy
<div className="page-spinner" aria-busy="true">Loading...</div>
Cause: Your page takes longer than 12 seconds to render meaningful content — usually due to slow API calls, large data fetches, or heavy client-side computation.Fix: Optimize data loading for your critical content path:
  • Fetch only the data needed for initial render
  • Defer non-essential data (comments, recommendations) to load after primary content
Cause: Your app checks authentication or data, then redirects to a 404 or login page before content loads.Fix:
  • Ensure public pages don’t require authentication
  • Don’t redirect before content is fetched
  • Return proper 404 status codes (not redirects to a 404 page)
// Bad - redirects before checking if page exists
if (!user) {
  navigate('/login');
  return null;
}

// Good - show content for public pages regardless of auth
if (isPublicPage) {
  return <PageContent />;
}
Cause: Code-split bundles fail to load during pre-rendering. This typically happens when a new deployment changes chunk hashes but old URLs are still being rendered.Common errors:
  • ChunkLoadError
  • Loading chunk [name] failed
  • Failed to fetch dynamically imported module
Fix:
  • Ensure all JS chunks are available at the URLs your app references
  • If using hashed filenames, make sure old chunks remain accessible during deployment transitions
  • Add error boundaries to gracefully handle chunk failures
Cause: Your page has fewer than 100 characters of visible text or fewer than 20 DOM elements. The renderer keeps waiting for more content until it times out.This commonly happens with:
  • Pages that are mostly images or video with minimal text
  • Single-purpose pages (e.g., a login form with just a few fields)
  • Pages where content is rendered inside <canvas> or <iframe>
Fix: Add visible text content that’s relevant for SEO — a heading, description, or alt text that brings the page above the 100-character threshold.

After Fixing

Once you’ve fixed the issue in your code:
  1. Go to your Hado SEO Dashboard
  2. Navigate to the affected domain
  3. Click Recrawl to trigger a fresh pre-render
  4. Verify with SEO Bot Crawler Test