A site can look perfect in a browser and be almost invisible to a crawler. That gap is what a JavaScript SEO audit exists to close — checking whether the content, links, and metadata your framework produces at runtime actually survive into Google’s index, and finding the specific place they get lost when they do not.
Google renders JavaScript. That has been true for years and it is not the argument anymore. The argument is about cost, timing, and edge cases: rendering happens in a second pass that can lag the initial crawl, it burns crawl budget, and any failure in that pass fails silently. On a 50-page marketing site the risk is small. On a 40,000-URL storefront it is the difference between indexed and not.
Start with the two-view comparison
Every audit begins the same way. Compare what the server sends against what the browser builds.
Fetch the raw HTML with curl or view-source and search it for a distinctive sentence from the middle of your main content. Then open the same page in DevTools and check the rendered DOM in the Elements panel. Four outcomes are possible: content in both (server-rendered, ideal), content only in the DOM (client-rendered, needs verification), content in neither (broken), or content in the source but replaced after hydration (the sneaky one).
Do this for one page of each template type — home, category, product or article, paginated listing, filtered view. Five checks, ten minutes, and it tells you the shape of the whole problem. Then confirm with Google’s own view: the URL Inspection tool in Search Console, “Test live URL,” then “View crawled page.” That is Googlebot’s rendering, not Chrome’s, and the two do diverge.
The failure modes worth checking by name
Most JavaScript SEO problems are one of a handful of recurring patterns.
- Links that are not links. A div with an onClick handler is not a link. Googlebot follows
<a href>with a real URL and nothing else. Router-driven navigation must still emit anchor tags with hrefs. - Content behind interaction. Tabs, accordions, and “load more” buttons that fetch on click hide their payload from the crawler. If it matters for ranking, it must be in the DOM without a click.
- Infinite scroll with no paginated fallback. Scroll events never fire for a crawler. Provide real paginated URLs behind the scroll.
- Client-set canonicals and meta tags. Google honors the canonical it finds in the rendered HTML, but injecting or rewriting canonicals late is a common source of the wrong URL being chosen. Serve them from the server.
- Soft 404s. A missing product that returns HTTP 200 with a JavaScript-rendered “not found” message gets indexed as a thin page. Return a real 404 status.
- Blocked resources. A robots.txt rule that disallows /static/ or /_next/ prevents the render pass from completing. Check this first; it is the cheapest catastrophic bug.
Hydration mismatches and the content swap
The hardest bug to spot is the one where server HTML is correct and the client replaces it with something worse. This happens when a framework’s hydration step disagrees with the server output — a personalization layer, a feature flag, a locale detector, or a component that renders a loading skeleton before data arrives.
The symptom in Search Console is a page with impressions for terms that do not appear on the live page, or a rendered screenshot that shows placeholders where text should be. To reproduce, load the page with JavaScript disabled, screenshot it, then load it normally and compare. If the disabled-JS version has content that the enabled version removes or reorders significantly, you have a mismatch worth fixing at the framework level.
Also watch for geolocation and cookie-based redirects. Googlebot crawls predominantly from US IPs and does not carry cookies between requests. A site that redirects unknown visitors to a region selector will show Google that selector on every URL.
Crawling the site the way a bot does
Spot checks find the templates that are broken. A crawl finds the scale of it. Run two crawls of the same site — one with JavaScript rendering off, one with it on — and diff the results. The URLs that appear only in the rendered crawl are pages that depend entirely on client-side execution to be discovered. That set is your risk surface.
Watch four numbers across the two crawls: total URLs discovered, pages with a missing or empty title, pages with word count under a few hundred, and internal links per page. Large discrepancies in any of them point at a specific template. Pair that with Search Console’s Pages report — “Discovered, currently not indexed” at high volume on a JS-heavy site usually means the render queue is not keeping up with the URLs you are producing.
For sites where you want a fast read on the basics rather than a full technical crawl, SEO Rocket runs an instant quick scan of roughly 25 pages and deep full-site crawls verified past 900 pages, reporting concrete evidence per issue — the actual title, URL, and H1 text, not a count — plus PageSpeed and Core Web Vitals with real-user field data. It is a solid first pass for finding missing titles, thin pages, and broken structure. Diffing rendered versus unrendered crawls is a job for a dedicated desktop crawler.
Rendering strategy: pick one and commit
Once you know what is broken, the fix is almost always architectural.
- Static generation for anything that does not change per request — blog posts, docs, marketing pages, most category pages. Fastest and the least fragile.
- Server-side rendering for content that is dynamic but must be indexed — product pages with live pricing, search-result landing pages.
- Client-side rendering for anything behind a login or irrelevant to search — dashboards, account settings, cart.
- Dynamic rendering — serving pre-rendered HTML to bots only — as a stopgap. Google has moved away from recommending it, and it adds a whole system to maintain. Use it to buy time, not as a destination.
The decision is per-template, not per-site. A single application can and should mix all three.
Speed is part of the audit
JavaScript-heavy pages fail Core Web Vitals in predictable ways. Large bundles push out Largest Contentful Paint because the hero content waits on hydration. Third-party tag managers destroy Interaction to Next Paint. Layout shifts come from components that render after the fold has already painted.
Use field data over lab data when you have it. A Lighthouse score from your laptop on office wifi is not what your visitors experience. Real-user metrics tell you which of the three vitals is actually failing and for what share of sessions, and that tells you whether to attack bundle size, main-thread work, or reserved layout space.
A repeatable audit sequence
- Confirm robots.txt does not block scripts, styles, or API routes needed to render.
- Run the source-versus-DOM comparison on one page per template.
- Inspect three live URLs in Search Console and read the rendered HTML Google produced.
- Crawl with and without rendering; diff URLs, titles, word counts, and link counts.
- Check status codes — no soft 404s, no 200s on missing entities.
- Verify canonicals, hreflang, and structured data appear in the rendered output and match the server output.
- Pull Core Web Vitals field data and attribute each failing metric to a cause.
- Re-crawl four weeks after fixes; index coverage moves slowly, so judge on trend.
The point of a JavaScript SEO audit is not to prove the site is broken. It is to find the two or three templates where the rendering pipeline drops something important, fix those, and stop guessing about the rest.