JavaScript SEO
Is Googlebot Actually Seeing What Your React or Next.js Site Renders?
JavaScript SEO means closing the gap between what a browser renders after your JS runs and what a crawler indexes. On a client-rendered or partially-hydrated site, those two can diverge — content that's visible to a visitor can be invisible, delayed, or truncated for the crawler that decides whether the page ranks at all.
The problem
Most technical SEO advice assumes the HTML a server sends is the content search engines see. On JavaScript-rendered sites that assumption breaks: Googlebot has to execute your JS in a second rendering pass before it can index anything client-rendered, that pass is queued and can be delayed by days on a large or infrequently-crawled site, and any JS error, unhandled promise rejection, or blocked resource during that pass can silently drop content Google never retries. A page can look complete in a browser and still be indexed as an empty shell.
Symptoms
Google Search Console's URL Inspection tool shows a rendered screenshot that's missing content visible in a normal browser, or the "Page fetch" step succeeds while "Screenshot" or "More info" flags render-blocked resources.
The indexed content of a page (checked via `site:` search or the Inspection tool's rendered HTML) lags noticeably behind what's currently live, even weeks after a content update.
Content behind a client-side fetch (data loaded after mount, e.g. `useEffect` + `fetch`) never appears in Google's cached/rendered HTML at all.
Internal links rendered only via `onClick` handlers or JS-injected `<a>` tags without a real `href` aren't discovered by crawlers that don't execute every event handler.
Core metadata (title, canonical, meta description) is set client-side after hydration instead of present in the initial server response, so it's inconsistent between what curl/view-source shows and what the browser eventually renders.
How we diagnose it
We start with the same test that matters most: fetch the URL the way a crawler does (no browser, no JS execution — plain HTTP GET) and diff that against the fully rendered DOM in a headless browser. Anything present in the rendered DOM but absent from the raw response is a candidate for a render-dependent indexing gap. We cross-check with Search Console's URL Inspection rendered screenshot and "View Crawled Page" HTML, check the Network tab for render-blocking or failed requests during first paint, and review Core Web Vitals field data (see our Core Web Vitals page) since slow hydration and JS SEO problems frequently share a root cause.
The fix
The fix is almost always to move content into the initial server response rather than leaving it dependent on client-side JS execution — server-side rendering, static generation, or streaming server components for anything that matters for indexing (headings, body copy, canonical/meta tags, internal links with real hrefs). Content that's genuinely interactive-only (a dashboard behind auth, for example) doesn't need this; content meant to rank does. We also make sure critical resources (fonts, above-the-fold images, render-blocking scripts) aren't accidentally disallowed in robots.txt, since a blocked resource during Google's rendering pass can cause the page to be indexed as incomplete even when the HTML itself is fine.
Implementation details
This site is itself built on Next.js's App Router: every canonical page under `src/app/[lang]/[...slug]/page.tsx` is a Server Component whose `generateMetadata()` resolves title, description, canonical and hreflang tags server-side before any client JS runs — the same pattern this page describes rather than something we only recommend to clients. Page bodies then hand off to client components (the `"use client"` files under `src/components/PageContent/`) only for interactivity (animations, accordions), not for the text content itself, which arrives in the initial HTML via the translations JSON consumed server-side. For Next.js sites specifically we check: the App Router's static vs. dynamic rendering choice per route (`generateStaticParams` coverage), whether client components are hydration-only wrappers around server-rendered content versus a client-rendered island that swallows content until JS loads, and whether `next/dynamic` imports used for code-splitting (see the `dynamic-loaders` pattern used throughout this codebase) still render server-side (`ssr: true`) rather than client-only.
Common mistakes
Treating "it renders fine in Chrome DevTools" as proof it's indexable — that only confirms the browser can execute the JS, not that Google's rendering pass will pick it up on the same schedule or budget.
Fetching primary page content client-side (`useEffect` + API call) instead of server-side, so there's a real chance a crawler indexes the loading state instead of the content.
Using `next/dynamic(..., { ssr: false })` for content that matters for SEO, which opts the whole chunk out of server rendering, not just its interactivity.
Blocking JS/CSS bundles in robots.txt (a legacy practice from the pre-rendering-crawler era) that actively prevents Google from rendering the page correctly today.
Assuming a JS framework migration (e.g. moving to a new meta-framework or app architecture) preserves SEO by default — rendering strategy, metadata generation, and route structure all have to be re-verified, not assumed.
Validation
We validate with Search Console's URL Inspection tool (both the rendered screenshot and "View Crawled Page" HTML) on a sample of real URLs, a raw-HTTP-vs-rendered-DOM diff across the site's main templates, a robots.txt check for accidentally blocked JS/CSS, and Core Web Vitals field data to confirm the fix didn't just move content into the DOM but also kept it fast enough for INP.
Bilingual JS SEO considerations
On a bilingual EN/AR site, JavaScript SEO issues can compound with locale routing: content fetched client-side after a locale switch, or hreflang alternates computed only in a client effect, can leave one language's rendered response incomplete even when the other locale's is fine. We verify both locales' raw HTTP responses independently, not just one and assume symmetry.
Related service & solutions
JavaScript SEO is the rendering layer underneath schema markup, hreflang, and canonical tags — none of those signals matter if the content and metadata that carry them never reach the crawler intact.
Frequently asked questions
Common questions about JavaScript SEO
Doesn't Google render JavaScript now, so this isn't a problem anymore?
Is server-side rendering always the right fix?
Can you fix JavaScript SEO without a framework migration?
How is this different from Core Web Vitals work?
Want your JavaScript rendering checked against what Google actually indexes?
Request a JavaScript SEO auditYou Might Also Find Useful
How Long Does SEO Take to Work?
SEO takes 3–6 months to show results and 9–12 months for full ROI. Learn the honest timeline, what affects speed, and how to accelerate results in Jordan.
How Much Does SEO Cost? — Pricing Guide for Jordan & MENA
SEO in Jordan costs $500–$1,500/month for meaningful results. Learn what drives pricing, what you're actually paying for, and how to avoid low-quality SEO traps.
SEO vs GEO: Traditional Search vs AI-Powered Discovery
Learn the difference between SEO (traditional search) and GEO (generative engine optimization for AI) and how to build a strategy that wins on both channels.