JavaScript SEO for React and Next.js Sites: A Practical Fix List
JavaScript SEO is the work of making sure search engines and AI crawlers can find, render and index pages built with React, Next.js and similar frameworks. Google renders JavaScript, but rendering can be delayed, and most AI crawlers read only the first HTML. The fix is to put content, links, titles and canonicals in the server HTML.
What is JavaScript SEO?
JavaScript SEO makes sure that the parts of a page Google needs are there without waiting for JavaScript to run. That means the main content, the H1, the links, the title, the canonical and the robots tag. If they only appear after scripts run, you depend on a slower step that can fail.
This guide is a ranked fix list. Each fix has a 30-second test, the broken pattern and the fixed pattern.
The short fix list
|
# |
Fix |
Quick test |
Impact |
Who fixes it |
|
1 |
Main content and H1 in the server HTML |
View Source, search for your H1 |
High |
Developer |
|
2 |
Real `<a href>` links, not click handlers |
View Source, count links in the menu |
High |
Developer |
|
3 |
Tabs, accordions and “load more” content reachable |
Search View Source for hidden text |
Medium |
Developer |
|
4 |
Unique title and description per route |
View Source on two different pages |
High |
Developer and marketer |
|
5 |
Canonical in the server HTML, never changed by JavaScript |
Compare View Source with URL Inspection |
High |
Developer |
|
6 |
No `noindex` in the first HTML on pages you want indexed |
View Source, search for “noindex” |
High |
Developer |
|
7 |
Real 404 status codes and server-side redirects |
Open a fake URL and check the status code |
Medium |
Developer |
|
8 |
History API URLs, not `#` fragments |
Click through the site and watch the address bar |
Medium |
Developer |
|
9 |
JavaScript and CSS files not blocked in robots.txt |
robots.txt plus URL Inspection resources |
Medium |
Developer |
|
10 |
Lazy-loaded images and infinite scroll that Google can see |
URL Inspection rendered HTML |
Medium |
Developer |
How Google handles a React or Next.js page
Google processes JavaScript pages in three phases: crawling, rendering and indexing. It first fetches the raw HTML. Later, a headless Chromium renders the page and runs your scripts, and only then can content added by JavaScript be indexed.
The gap between those steps is the real risk. In a Vercel and MERJ study of over 100,000 Googlebot fetches on nextjs.org, every indexable HTML page got a full render. The median render delay was 10 seconds and the 75th percentile was 26 seconds. But the 90th percentile was around 3 hours, and the 95th around 6 hours.
So Google will usually see your JavaScript content, just sometimes late. Anything that breaks during rendering may never be seen. For the full journey from crawl to ranking, see our guide to how search engines crawl, index and rank content.
The 10-minute test: what Google really sees
Run four checks on one page of each template, such as a service page, a blog post and a product page. Any gap between what the server sends and what the browser builds is where your fixes start.

- View Source. Right-click the page and pick View Page Source. This is the first HTML, before any JavaScript runs. Search it for your H1, a body sentence, your title and “canonical”.
- Inspect. Open Chrome DevTools and look at the Elements panel. This is the rendered DOM, after JavaScript runs. If something is here but missing from View Source, JavaScript added it.
- URL Inspection. In Google Search Console, inspect the URL and run a live test. Open the rendered HTML and the page resources list. This shows what Google actually built and which files it could not load.
- A no-JavaScript crawl. Crawl the site with a crawler set to plain HTML, with JavaScript rendering turned off. Then crawl it again with rendering on. Pages, links or titles that only show up in the second crawl depend on JavaScript.
Fixes 1 to 3: content, H1 and links that only exist after JavaScript runs
A common problem with client-only React is an empty shell: View Source shows one empty div and a script tag. Google must render the page to find any words or links, and most AI crawlers do not.
Fix 1: put the main content and H1 in the server HTML
Quick test: search View Source for your H1 text. If it is not there, this fix is your top priority.
Broken pattern: the page fetches its content in `useEffect` after the first load, so the server sends an empty container.
Fixed pattern: render the content on the server or at build time. In the Next.js App Router, pages are Server Components by default, so fetch the data in the page itself and keep `’use client’` for small interactive parts. On a plain React site, prerender your key pages to static HTML at build time (see the React vs Next.js section below).
Fix 2: use real `<a href>` links
Quick test: in View Source, look at your main menu and footer. Each link should be an `<a>` element with an `href` that holds a real URL.
Broken pattern: a `div` or `button` with an `onClick` that calls the router. Crawlers do not click.
Fixed pattern: Google can only reliably find links that are `<a>` elements with an `href`, as its link best practices explain. In Next.js, `next/link` renders a proper `<a href>`. In React Router, use its `Link` component rather than a click handler.
Fix 3: make tabs, accordions and “load more” content reachable
Quick test: pick a sentence inside a closed tab or accordion and search View Source for it.
Broken pattern: the content is only fetched when someone clicks the tab, or the next set of products only loads when someone presses “load more”.
Fixed pattern: keep tab and accordion content in the HTML and hide it with CSS until it is opened. For “load more”, give each batch its own URL and link to it. Google Search does not interact with pages, so content that needs a click may never be seen.
Fixes 4 to 6: titles, meta descriptions, canonicals and the noindex trap
Wrong or missing titles in Google results can come from metadata set in the browser. Send each page’s title, description, canonical and robots tag in the first HTML instead.
Fix 4: give every route a unique title and description
Quick test: open View Source on two different pages. If both show the same title, or the home page title, your metadata is set in the browser.
Broken pattern: one title in `index.html` for the whole app, changed later by a client-side library.
Fixed pattern by stack:
- Next.js App Router: export a `metadata` object or a `generateMetadata` function from each `page` or `layout`. Use `title.template` in the root layout for a consistent brand suffix. Next.js only supports these exports in Server Components, so they never work in a file marked `’use client’`. Keep the page as a Server Component and move the interactive part into its own file.
- React 19: you can render a `<title>` or `<meta>` tag anywhere in a component, and React places it in the document head. Render only one title at a time, and pass it a single string. On a client-only site this still runs in the browser, so it only helps the first HTML if you also prerender or server-render.
- **react-helmet-async:** on React 16 to 18 it writes tags into the server response only if you render on the server. Its docs say that on React 19 its server context is no longer filled, and suggest rendering the tags in your components instead.
Fix 5: keep the canonical in the server HTML
Quick test: compare the canonical in View Source with the one in URL Inspection’s rendered HTML. They should be identical.
Broken pattern: no canonical in the first HTML, or one canonical in the HTML that JavaScript later swaps for a different URL.
Fixed pattern: send the right canonical from the server. Google’s JavaScript SEO basics warn against using JavaScript to change the canonical to something other than what the original HTML says. In Next.js, set `alternates.canonical` in your metadata.
Fix 6: avoid the noindex trap
Quick test: search View Source for “noindex” on a page you want in Google.
Broken pattern: the server sends a `noindex` tag as a placeholder, and JavaScript removes it once the content loads.
Fixed pattern: when Google finds `noindex`, it may skip rendering, so the script that removes the tag never runs. If you want a page indexed, leave `noindex` out of the original code. Set the robots tag on the server for each route.
Fixes 7 and 8: real 404s, redirects and clean URLs
Single-page apps often answer every URL with a 200 status, so Google sees soft 404s: error pages that claim to be fine. Clean URLs matter too, because Google may not reliably treat `#` fragment URLs as separate pages.
Fix 7: return real 404s and server-side redirects
Quick test: open a made-up URL such as /this-does-not-exist on your site. Check the status code in the DevTools Network tab. It should say 404, not 200.
Broken pattern: the React router shows a “page not found” component, but the server already sent a 200.
Fixed pattern:
- Next.js: call `notFound()` when a record does not exist. It renders your `not-found` page and adds a `noindex` robots tag. Call it before streaming starts to get a real 404 status. If a Suspense boundary has already started streaming, the status stays 200, though the `noindex` tag is still added. For moved pages, use `permanentRedirect()` (a 308, per the redirect docs) or redirects in `next.config.js`.
- Plain React: Google’s guide to fixing search-related JavaScript problems and its basics page suggest two options. Either redirect with JavaScript to a URL that returns a real 404 from the server, or add a `noindex` robots tag to the error page with JavaScript. A server or host rule that returns 404 for unknown paths is cleaner still.
Fix 8: use the History API, not hash fragments
Quick test: if URLs look like example.com/#/services as you click around, you have hash routing.
Broken pattern: a hash router, where every view lives after a `#`. Google may not reliably treat fragments as separate pages.
Fixed pattern: switch to a browser router that uses the History API, so each view has a real path like /services. Make sure the server can answer each path directly, not only through in-app navigation.
Fixes 9 and 10: blocked files, lazy-loaded images and infinite scroll
If Google cannot load your scripts, the page can render empty. If images only load on scroll, Google may not see them.
Fix 9: do not block JavaScript or CSS
Quick test: read your robots.txt for rules that block script or style folders. Then check the page resources list in URL Inspection for blocked files.
Broken pattern: a robots.txt rule that blocks a build folder such as `/_next/` or `/static/js/`. Google will not render JavaScript from blocked files.
Fixed pattern: allow all files the page needs to render. Also use fingerprinted file names, with a content hash in the name. Google’s renderer may ignore caching headers, so a new file name stops it from using an old script.
Fix 10: make lazy loading and infinite scroll visible to Google
Quick test: in URL Inspection, open the rendered HTML. Check that images further down the page have real URLs in their `src` attributes.
Broken pattern: images or products that only load when the user scrolls, using scroll events.
Fixed pattern: load content when it enters the viewport, using the browser’s built-in lazy loading (`loading=”lazy”`) or the IntersectionObserver API. Google’s lazy-loading guide says to give each chunk of an infinite scroll its own persistent URL with an absolute page number, such as ?page=12, and link those pages in order.
Plain React without a rebuild vs moving to Next.js
You do not have to move to Next.js to get your pages indexed. A plain React site built with Vite or Create React App can prerender its public pages to static HTML at build time. Moving to Next.js makes sense when you have many dynamic pages, such as products or listings, that change often.

Here is how we would treat common page types:
|
Page type |
Plain React (Vite or CRA) |
Next.js App Router |
|
Home, service and about pages |
Prerender to static HTML at build time |
Static rendering (the default) |
|
Blog posts |
Prerender at build time, rebuild on publish |
Static rendering with revalidation |
|
Product or listing pages |
Hard to prerender at scale, a strong reason to move |
Server rendering or static with revalidation |
|
Search results with filters |
Usually keep out of the index |
Server rendering for the few indexable filters |
|
Account, cart and dashboards |
Client-side rendering is fine |
Client components, kept out of the index |
Google calls dynamic rendering, where bots get a different prerendered version, a workaround and not a recommended solution. It suggests server rendering, static rendering or hydration instead. If a rebuild is on the table, it makes sense to bundle SEO fixes into planned upgrade work. Our web development team can scope that kind of move.
Next.js App Router checklist
In the App Router, pages render on the server by default, so content and links are in the first HTML. Beyond fixes 4 and 7, check these traps before launch.
- Streaming metadata. Since version 15.2, on pages with dynamic metadata, Next.js can stream the tags from `generateMetadata` into the body after the first HTML. Prerenderable pages still carry them in the head. The docs say Googlebot reads streamed tags, and bots on the `htmlLimitedBots` list get them in the head. If an AI or social crawler matters to you, check that list.
- `sitemap.ts` and `robots.ts`. Add a sitemap file that lists every indexable URL from your data source. Add a robots file in the root of `app` and point it at the sitemap.
- JSON-LD. Render structured data as a `<script>` tag inside your page or layout, as the Next.js JSON-LD guide recommends. For UAE shops, set the Product offer’s price currency to AED.
- Proxy redirects. In Next.js 16, the `middleware` file is now called `proxy`. Use a matcher that skips `_next/static`, images, `robots.txt` and `sitemap.xml`, or your redirect logic can block the files Google needs.
Most AI crawlers do not run JavaScript
Most AI crawlers read only the first HTML your server sends. Vercel’s 2024 research found that none of the major AI crawlers it tracked rendered JavaScript. If your content only appears after scripts run, tools like ChatGPT and Perplexity may see an empty page.

The research names OpenAI’s crawlers, ClaudeBot, PerplexityBot, Meta’s crawler, Bytespider and Common Crawl. The ChatGPT and Claude crawlers download JavaScript files but do not run them. In the same research, Gemini rendered because it uses Google’s own setup, and AppleBot rendered too.
No separate fix is needed. Fixes 1, 2, 4 and 5 put content, links and metadata in the server HTML, which is what these crawlers read.
UAE sites: Arabic and English routes, hreflang, RTL and bot rules
Bilingual UAE sites need a crawlable URL per language, matching hreflang tags and text direction set on the server. Your security setup must also let Googlebot in.
Give each language its own URL
Use sub-paths such as /en/ and /ar/, with every page nested under a locale segment like `app/[lang]`, as the Next.js internationalization guide shows. Avoid one URL that swaps language with JavaScript or cookies. Google recommends separate URLs for each locale, marked up with hreflang.
Add hreflang for ar-AE and en-AE
Each version must list itself and every other version, or Google may ignore the tags. Use `ar-AE` and `en-AE`, which pair an ISO 639-1 language code with an ISO 3166-1 region code. Add `x-default` for the page shown when no language matches. In Next.js, set these with `alternates.languages` in your metadata, so they ship in the server HTML. Google’s guide to localized versions covers the rules in full.
Set lang and dir on the server
Set `lang=”ar-AE”` and `dir=”rtl”` on the html element in the server-rendered layout. Do not switch them in the browser after load. If the server and the browser disagree on direction or language, React can throw hydration mismatch errors, and the server HTML carries the wrong language.
Be careful with locale redirects
Many sites redirect the root URL to /en/ or /ar/ based on the browser’s language. Google says Googlebot sends requests without an Accept-Language header, and its default IP addresses appear to be in the USA, though it also crawls from some other countries. So you cannot rely on a language or location redirect to show Googlebot every version. Keep both versions linked from every page, and never redirect a locale URL to another locale.
Check that your WAF lets Googlebot in
Cloudflare, AWS WAF and similar tools can block or challenge Googlebot if bot rules are too strict. Look at the Crawl stats report in Search Console for sudden rises in errors. Cloudflare classifies Googlebot as a verified bot in its search engine crawler category, so review your bot rules against that category. To confirm a visitor really is Google, do not trust the user agent string alone. Use the reverse DNS check Google describes.
The same checks apply to headless stores built on Shopify, Salla or Zid. The storefront is usually a React or Next.js app, so the full fix list applies to it.
Where to start
Run the 10-minute test on one page per template, then work down the fix list. Start with the high-impact fixes.
If you want a second pair of eyes, our technical SEO services team works with developers on React and Next.js sites. A technical SEO audit is the natural next step: it compares server HTML with what Google renders and ranks the gaps for your developers.
FAQ
JavaScript SEO is making sure search engines and AI crawlers can find, render and index pages built with JavaScript frameworks. It focuses on putting content, links, titles, canonicals and robots tags in the HTML the server sends. It also covers status codes, URLs and files that Google needs to render the page.
React can be good for SEO when pages are rendered on the server or prerendered at build time. A client-only React site sends an almost empty HTML page, so Google has to render it first and most AI crawlers see nothing. Prerendering your public pages fixes most of this without a full rebuild.
Next.js is easier to get right for SEO because its App Router renders pages on the server by default. It also has built-in tools for metadata, sitemaps, robots files and 404 pages. A plain React site can reach the same result, but it needs extra setup such as prerendering.
Yes, Google renders JavaScript and indexes content that appears after scripts run. Rendering can be delayed, from seconds to hours in some cases. Content in the first HTML does not depend on that step.
Usually not. Google calls dynamic rendering a workaround and not a recommended solution. It suggests server rendering, static rendering or hydration instead.
In a Vercel and MERJ study of nextjs.org, the median render delay was 10 seconds and three quarters rendered within 26 seconds. The slowest ten percent took around three hours or more. Pages with content in the server HTML do not have to wait for that step.
You do not need to code to find most JavaScript SEO problems. View Source, URL Inspection and a crawler will show you what is missing. The fixes themselves usually need a developer, so a clear, ranked list helps both sides.
Sources
- Google Search Central: Understand JavaScript SEO basics
- Google Search Central: Dynamic rendering as a workaround
- Google Search Central: SEO link best practices
- Google Search Central: Fix lazy-loaded website content
- Google Search Central: Fix search-related JavaScript problems
- Google Search Central: Tell Google about localized versions of your page
- Google Search Central: How Google crawls locale-adaptive pages
- Google Search Central: Verifying Googlebot and other Google crawlers
- Vercel: How Google handles JavaScript throughout the indexing process
- Vercel: The rise of the AI crawler
- Next.js docs: generateMetadata
- Next.js docs: notFound
- Next.js docs: redirect
- Next.js docs: sitemap.xml
- Next.js docs: robots.txt
- Next.js docs: How to implement JSON-LD
- Next.js docs: proxy.js
- Next.js docs: Internationalization
- React docs: title
- react-helmet-async on GitHub
- Cloudflare docs: Verified bots
