Encited logo
PricingTalk to sales
JavaScript Redirects and Links: What Counts for SEO and What Crawlers Skip

JavaScript Redirects and Links: What Counts for SEO and What Crawlers Skip

Oct 29, 2026October 29, 2026ยทby Aki from Encited

When a JavaScript redirect counts for Google and when crawlers miss it, why onclick links go unfollowed, hash and #! routes, injected links, and the SPA fix.

TL;DR. Google follows a JavaScript redirect and a JavaScript link only after it renders the page, and rendering can fail or lag the crawl by hours. AI crawlers never render, so a window.location redirect and an onclick link do not exist for them. Google's rule for links is an <a> element with an href; its rule for redirects is a server-side 301 whenever you can. On a single-page app, keep the router, render real anchors, put permanent redirects at the server, and return a 404 status for unknown routes.

A redirect written in JavaScript runs after the page loads, and a link wired to a click handler works only after your bundle runs. Googlebot runs that JavaScript in a second pass after the crawl; the crawlers behind ChatGPT, Claude and Perplexity never run it. That difference decides most of JavaScript redirect SEO and JavaScript links SEO: a signal that exists only in the rendered page reaches Google late and reaches AI search never. The wider topic is in our guide to JavaScript SEO.

Do JavaScript redirects count for SEO?

Google lists "JavaScript location" as a permanent redirect type in its redirects documentation, so a window.location redirect can pass a page's signals to the new URL. It sits fifth in Google's table of permanent types, after HTTP 301, HTTP 308, meta refresh (0 seconds) and HTTP refresh (0 seconds), because "Google Search interprets and executes JavaScript using the Web Rendering Service once crawling of the URL has completed." Google's advice: "Only use JavaScript redirects if you can't do server-side or meta refresh redirects."

Two conditions have to hold: Google has to render the page, and the render has to succeed. Google's page names the failure case: "This means that if you set a JavaScript redirect, Google might never see it if rendering of the content failed." Rendering also waits in a queue, and what Google waits for before it renders is in how Google renders JavaScript. Vercel and MERJ measured that wait on nextjs.org in April 2024 and published it on July 31, 2024: a median of 10 seconds from crawl to render, with the slowest 1% waiting about 18 hours.

The redirect never counts for AI crawlers. Vercel and MERJ's December 17, 2024 crawler study found that "none of the major AI crawlers currently render JavaScript", covering OpenAI's GPTBot, OAI-SearchBot and ChatGPT-User, Anthropic's ClaudeBot and PerplexityBot. For each of them the old URL returns a 200 with the old page, and the script that would send them on never runs.

window.location vs an HTTP 301

A window.location redirect lives in the page body; an HTTP 301 lives in the response headers, before any body arrives. The JavaScript version:

html
CopyDownload
<script>
window.location.replace("https://yoursite.com/new-location");
</script>

The HTTP version is a status line and one header, sent by your server, host or CDN:

http
CopyDownload
HTTP/1.1 301 Moved Permanently
Location: https://yoursite.com/new-location

Every crawler reads the 301 on its first request, with no render and no queue, and Google calls a server-side redirect "the best way to ensure that Google Search and people are directed to the correct page." Vercel, Netlify and Cloudflare Pages each take a redirect list in a config file, so a static host sends the same header.

window.location HTTP 301
Googlebot on the first crawl Sees a 200 and the old page Sees the redirect
Googlebot after rendering Follows it, if the render succeeds Already followed
GPTBot, ClaudeBot, PerplexityBot Never runs it Follows it

To see which one you have, fetch the old URL with the Googlebot simulator. A 301 shows in the status line with its Location header. A 200 whose body is your app shell means the redirect is JavaScript only. The same check with curl, URL Inspection and the crawl log is written out in how to test JavaScript SEO.

Google's link best practices draw one line: "Google can only crawl your link if it's an <a> HTML element (also known as anchor element) with an href attribute." An onclick handler is fine as long as the href is there: Google's recommended list includes <a href="/products/category/shoes" onclick="javascript:goTo('shoes')">. Its not-recommended list has <a routerLink="products/category">, <span href="https://example.com"> and <a onclick="goto('https://example.com')">, and it marks <a href="javascript:goTo('products')"> as a URL it cannot resolve.

A link that fails the rule:

html
CopyDownload
<div class="nav-item" onclick="goTo('/pricing')">Pricing</div>

The same link, fixed:

html
CopyDownload
<a href="/pricing" onclick="goTo('/pricing'); return false;">Pricing</a>

The crawler reads /pricing from the href. The browser runs goTo, and return false stops the full page load, so people keep the client-side transition. Router components do this for you: Link in React Router and Next.js, RouterLink in Vue Router and routerLink in Angular all render an <a> with a real href.

A URL that changes only after # is the same URL to a crawler, because the browser never sends the fragment to the server. yoursite.com/#/pricing and yoursite.com/#/about both request yoursite.com/, and Google indexes one page. Google's JavaScript SEO basics say: "To ensure that Googlebot can parse and extract your URLs, don't use fragments to load different page content." The bad-practice example on that page is <a href="#/products">Our products</a>.

The #! form is older and no better. Google once ran an AJAX crawling scheme that fetched a ?_escaped_fragment_= version of a #! URL, and announced its end in Deprecating our AJAX crawling scheme in October 2015. A #! route today is a fragment like any other.

Google's fix is the History API: "For single-page applications with client-side routing, use the History API to implement routing". That is one router setting: BrowserRouter in React Router, createWebHistory() in Vue Router, and the Angular router's default. A fragment that points at a section of the current page, like <a href="#faq">, harms nothing.

Google follows links that JavaScript adds, once the page is rendered. Its link documentation says "Links are also crawlable when you use JavaScript to insert them into a page dynamically", with the usual condition: the result in the rendered DOM has to be an <a> with an href. A menu that React mounts after load reaches Google after the render queue, with the delay from the first section.

Two cases stay out of the rendered DOM: a menu that mounts only on hover or click, because the renderer does neither, and a block that loads on a scroll event. And AI crawlers read none of the injected links. GPTBot, ClaudeBot and PerplexityBot take the HTML from the first response, so a site whose internal links all come from a client-side menu has no internal links for them, and every page outside your sitemap is unreachable. View-source shows what they get; the Elements panel shows what Google gets after rendering.

rel="nofollow" set by JavaScript

A nofollow that a script adds after load gives Google two versions of the link: a plain one in the raw HTML and a nofollow one in the rendered DOM. Google's link documentation says nothing about which version wins. The nearest rule it publishes is for the robots meta tag, in its JavaScript SEO basics: "You can use JavaScript to add a robots meta tag to a page or change its content." The limit sits in the same paragraph: "When Google encounters the noindex tag, it may skip rendering and JavaScript execution". A directive that depends on the render is a directive Google may never run.

Put nofollow, sponsored and ugc in the HTML your server sends. The common offenders are comment widgets that mark user links after load and affiliate components that add rel="sponsored" in a useEffect. For AI crawlers, an attribute added by script is absent in both directions: they neither see the link as nofollow nor see any link that JavaScript created.

What to use on a single-page app

Keep the client-side router and change three things: the anchors it renders, where permanent redirects live, and what unknown routes return.

Real anchors, intercepted on click. Every navigation link is an <a href> the crawler can read, and the router catches the click so people stay in the app:

js
CopyDownload
document.addEventListener("click", (event) => {
const link = event.target.closest("a[href]");
if (!link || link.origin !== location.origin) return;
event.preventDefault();
history.pushState({}, "", link.href);
renderRoute(link.pathname);
});

The framework link components do the same job with less code. What matters is the output: an href in the HTML, a pushState in the browser.

Server-side 301s for moved URLs. When a route moves, add the redirect at the server, host or CDN so the 301 arrives in headers. A redirect in the router can stay for people who already have the app open; the 301 is what crawlers read.

A 404 status for unknown routes. A host that serves index.html with a 200 for every path tells Google that /this-page-does-not-exist is a page. Return a 404 from the server for paths outside your route list. Where the host cannot, a pre-render layer can: with Encited's dynamic rendering, your not-found route sets <meta name="prerender-status-code" content="404"> and crawlers get a real 404 with the snapshot, and a health check before each render flags server errors, redirects and blocked pages so a broken page is never cached. The same layer serves AI crawlers the rendered page with its links in the HTML, which closes the injected-links gap without touching the app.

The other things that break a single-page app, from meta tags set after load to content behind user actions, are in our SPA SEO guide.

FAQ

Does Google follow JavaScript redirects? Yes, after rendering the page. Google lists "JavaScript location" among its permanent redirect types and says Google "might never see it if rendering of the content failed". A 301 is followed on the first request.

Can Google crawl onclick links? Only when the element is an <a> with an href. <a href="/pricing" onclick="..."> is on Google's recommended list. A <div onclick>, a <span href> or an <a> with no href is on its not-recommended list.

Are hash links bad for SEO? A hash that loads different page content is, because /#/pricing and /#/about request the same URL. Google's documentation says not to use fragments to load different page content. A hash that jumps to a section of the current page is fine.

Do AI crawlers follow JavaScript redirects or JavaScript links? No. Vercel and MERJ's December 17, 2024 study found that none of the major AI crawlers render JavaScript, so for GPTBot, ClaudeBot and PerplexityBot a window.location redirect never runs and a link added by script never exists.

Is a meta refresh better than a JavaScript redirect? Google says that where server-side redirects are not possible, meta refresh redirects "may be a viable alternative", and that it reads an instant meta refresh as a permanent redirect. It sits in the HTML, so it needs no render. A 301 is still the first choice.

Google
Gemini
ChatGPT
Claude
Grok
Meta
Perplexity

Get discovered anywhere search happens

Readable, citable, outranking pages.

Talk to sales
Avatar
How can we help?
Get instant answers to your questions or leave a message for an engineer will reach out
Ask AI about Encited
See our docs
Contact support
Leave a message
We'll get back to you soon
Avatar
Ask AI about Encited
Product questions, troubleshooting and
Thinking
Preview
Drop an image to attach
Powered by ReplyMaven