---
title: "JavaScript Redirects and Links SEO: What Google Follows"
url: https://encited.com/blog/javascript-redirects-and-links-seo
description: "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."
lang: en
---

Image: JavaScript Redirects and Links: What Counts for SEO and What Crawlers Skip (https://encited.com/blog/thumbnails/javascript-redirects-and-links-seo.webp)

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

Oct 29, 2026 October 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 (https://encited.com/blog/javascript-seo).

## Do JavaScript redirects count for SEO?

Google lists "JavaScript location" as a permanent redirect type in its redirects documentation (https://developers.google.com/search/docs/crawling-indexing/301-redirects), 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 (https://encited.com/blog/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 (https://vercel.com/blog/the-rise-of-the-ai-crawler) 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

Copy Download

< 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

Copy Download

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 (https://encited.com/free-tools/crawler-simulator/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 (https://encited.com/blog/test-javascript-seo).

## Which JavaScript links count for SEO

Google's link best practices (https://developers.google.com/search/docs/crawling-indexing/links-crawlable) 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

Copy Download

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

The same link, fixed:

html

Copy Download

< 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`.

## Hash links and `#!` routes

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 (https://developers.google.com/search/docs/crawling-indexing/javascript/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 (https://developers.google.com/search/blog/2015/10/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.

## Links injected after load

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

Copy Download

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 (https://encited.com/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 (https://encited.com/blog/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.

Image: Google (https://encited.com/crawler-logos/google-search.svg)

Image: Gemini (https://encited.com/crawler-logos/gemini.png)

Image: ChatGPT (https://encited.com/crawler-logos/chatgpt.svg)

Image: Claude (https://encited.com/crawler-logos/claude.svg)

Image: Grok (https://encited.com/crawler-logos/xai.svg)

Image: Meta (https://encited.com/crawler-logos/meta.svg)

Image: Perplexity (https://encited.com/crawler-logos/perplexity.png)

## Get discovered anywhere search happens

Readable, citable, outranking pages.

Talk to sales: https://encited.com/contact-sales

Image: Avatar (https://replymaven.com/api/uploads/Su6Q0nK63iZW0ZFJBRNWpmRuppVDLuab/98df5262-f992-45fa-a750-00a6c919baf9.png)

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 (https://lovablehtml.com/docs)

Contact support

Leave a message

We'll get back to you soon

Ask AI about Encited

Product questions, troubleshooting and

Thinking

Image: Preview

Drop an image to attach

Powered by ReplyMaven (https://replymaven.com/)

## Structured data

```json
[
  {
    "@context": "https://schema.org",
    "@type": "BreadcrumbList",
    "itemListElement": [
      {
        "@type": "ListItem",
        "position": 1,
        "name": "Home",
        "item": "https://encited.com/"
      },
      {
        "@type": "ListItem",
        "position": 2,
        "name": "Blog",
        "item": "https://encited.com/blog"
      },
      {
        "@type": "ListItem",
        "position": 3,
        "name": "JavaScript Redirects and Links: What Counts for SEO and What Crawlers Skip",
        "item": "https://encited.com/blog/javascript-redirects-and-links-seo"
      }
    ]
  },
  {
    "@context": "https://schema.org",
    "@type": "Article",
    "headline": "JavaScript Redirects and Links: What Counts for SEO and What Crawlers Skip",
    "description": "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.",
    "image": "https://encited.com/blog/thumbnails/javascript-redirects-and-links-seo.webp",
    "datePublished": "2026-10-29",
    "dateModified": "2026-10-29",
    "author": {
      "@type": "Person",
      "name": "Aki from Encited"
    },
    "publisher": {
      "@type": "Organization",
      "name": "Encited.com",
      "alternateName": "LovableHTML.com",
      "logo": {
        "@type": "ImageObject",
        "url": "https://encited.com/brand/logo.svg"
      }
    },
    "mainEntityOfPage": {
      "@type": "WebPage",
      "@id": "https://encited.com/blog/javascript-redirects-and-links-seo"
    }
  }
]
```