Why Query Parameters After Hash Symbols Don’t Exist to Google

TL;DR

URL fragments (everything after the # symbol) are never sent to servers and Google doesn’t index them as separate pages. If your site uses hash parameters for content filtering, pagination, or navigation, Google sees only the base URL and misses the filtered content entirely. Hash-based routing popular in early single-page applications creates pages that functionally don’t exist to search engines, leaving valuable long-tail traffic on the table.


The Conversation

Steve, senior developer at a real estate platform, React SPA with client-side routing, 40,000 listings, intermediate SEO knowledge, frustrated that filtered searches don’t rank: Our property search lets users filter by price, bedrooms, location, and property type. These filtered views have significant search volume. “3 bedroom homes in Seattle under 500k” is exactly what our filter shows. But we don’t rank for any of these filtered queries.

SDC SEO Brain: How are filtered states reflected in your URLs?

Steve: Hash parameters. The URL looks like /properties#bedrooms=3&city=seattle&maxprice=500000. Users can bookmark and share these URLs.

SDC SEO Brain: That’s your problem. Hash fragments are client-side only. Everything after the # symbol never reaches your server and Google doesn’t treat it as a separate URL. From Google’s perspective, /properties#bedrooms=3 and /properties#city=seattle and /properties are all the exact same page.

Steve: But the content changes based on the hash. Different properties display.

SDC SEO Brain: Your JavaScript reads the hash fragment after the page loads and filters content client-side. But when Google requests your URL, the hash fragment isn’t part of the request. Google sends a request for /properties, your server returns the default properties page, and Google indexes that default state. It never knows the filtered states exist.

Steve: So we have 40,000 listings and tons of filter combinations, but Google only sees one page?

SDC SEO Brain: Google sees your individual listing pages if they have their own URLs. But your filtered search results, the combination pages that should rank for queries like “2 bedroom condos in Bellevue,” those are invisible. Every filter combination resolves to the same /properties URL from Google’s perspective.

Steve: Why did we build it this way?

SDC SEO Brain: Hash routing was standard practice in early single-page applications. It allowed “navigation” without triggering server requests. The browser doesn’t make a new HTTP request when the hash changes, which made SPAs feel fast and app-like. The tradeoff was SEO, but developers in 2012-2015 often didn’t understand that tradeoff when choosing architecture.

Steve: This app was built in 2016. Makes sense.

SDC SEO Brain: The fix is converting to real URLs. Filtered states need to be accessible at URLs that servers respond to differently. /properties?bedrooms=3&city=seattle or /properties/seattle/3-bedroom or any structure where the parameters are in the path or query string, not after a hash.

Steve: Can’t Google render JavaScript and see what’s on the page after the hash change?

SDC SEO Brain: Google renders the page it requests, which is /properties with no hash. JavaScript runs, but your code checks window.location.hash to decide what to filter. When Google requests the page, window.location.hash is empty because the hash wasn’t part of the request. Your filtering code sees no parameters and shows default content.

Steve: What about window.location during Google’s render?

SDC SEO Brain: Google’s renderer sets window.location to the URL it requested. If it requested /properties, that’s what window.location shows. The hash fragment was never sent, so it doesn’t exist in the render environment. There’s no way for your JavaScript to know what hash parameters to apply.

Steve: We can’t make Google request URLs with hash parameters?

SDC SEO Brain: No. HTTP specifications say hash fragments are client-side only. Browsers strip the hash before sending requests. This isn’t a Google choice; it’s how the web fundamentally works. Hash fragments were designed for in-page anchor navigation, not for representing different content states.

Steve: Converting our entire routing system is a massive refactor.

SDC SEO Brain: It is. But you’re essentially invisible for every filtered search query right now. That’s potentially thousands of long-tail searches where competitors with real URLs rank and you don’t. The refactor cost needs to be weighed against the organic traffic you’re leaving on the table.

Steve: What’s the modern approach for SPAs?

SDC SEO Brain: HTML5 History API, specifically pushState and replaceState. These let you change the browser URL without triggering a full page reload, just like hash routing, but the URL changes are real paths or query strings that can be requested from servers. React Router, Vue Router, and other modern routing libraries default to this approach.

Steve: If we switch to real URLs, do we need server-side rendering?

SDC SEO Brain: Not necessarily, but it helps. Without SSR, Google requests /properties?bedrooms=3, your server returns the same base HTML, and JavaScript has to read the query parameters and render appropriate content. Google’s JavaScript rendering can handle this, but SSR gives Google the content immediately without relying on client-side rendering.

Steve: What’s the risk of relying on Google’s JavaScript rendering for filtered pages?

SDC SEO Brain: Rendering budget and timing. Google allocates limited resources to JavaScript rendering. Your filtered pages might not get rendered quickly, or Google might see intermediate loading states instead of final content. SSR eliminates this uncertainty by serving complete HTML.

Steve: We use Next.js. Could we add SSR for search pages specifically?

SDC SEO Brain: Next.js supports this well. You can use getServerSideProps to fetch filtered listings based on query parameters and render them server-side. The URL /properties?bedrooms=3&city=seattle would return pre-rendered HTML with those specific properties. Google receives complete content immediately.

Steve: What about the escaped fragment convention? I’ve seen #! in URLs.

SDC SEO Brain: Escaped fragments were Google’s workaround for hash-based AJAX applications, created around 2009. When Google saw #! it would request an equivalent escapedfragment URL. Google officially deprecated this in 2015. Don’t build around deprecated systems. Modern Google expects standard URLs and JavaScript rendering.

Steve: After we migrate to real URLs, how do we handle old hash URLs that might be bookmarked?

SDC SEO Brain: Client-side JavaScript can detect hash parameters and redirect to the equivalent query parameter URL. When someone visits /properties#bedrooms=3, your code reads the hash, constructs /properties?bedrooms=3, and redirects. This preserves bookmarks and shared links.

Steve: That redirect wouldn’t help with SEO though, right? Since Google never requests hash URLs.

SDC SEO Brain: Correct. The redirect only helps users with old bookmarks. For SEO, what matters is that your new URLs exist, are linked internally, and get crawled. Make sure your sitemap includes key filtered URL combinations and your internal linking exposes them to crawlers.

Steve: Should we create pages for every possible filter combination?

SDC SEO Brain: No. That would be millions of pages, most with no search volume. Focus on combinations with actual demand. “3 bedroom homes Seattle” has volume. “3 bedroom homes Seattle built 1987-1989 with green exterior” doesn’t. Use keyword research to identify which filter combinations deserve dedicated indexable pages.


FAQ

Q: Why doesn’t Google index URLs with hash fragments?
A: Hash fragments are client-side only per HTTP specifications. They’re never sent to servers in HTTP requests. Google requests the URL without the hash, so all hash variations appear as the same page. This is a web standard, not a Google limitation.

Q: Can JavaScript make hash-based content indexable?
A: No. When Google requests your page, the hash fragment doesn’t exist in the request. Your JavaScript can’t read hash parameters that were never sent. Even with JavaScript rendering, Google sees only the default state because no hash parameters are present.

Q: What’s the alternative to hash routing for SPAs?
A: Use HTML5 History API (pushState/replaceState) for client-side routing with real URLs. Modern frameworks like React Router and Vue Router default to this approach. URLs change in the browser without page reloads, but the paths are real server-accessible URLs.

Q: Do I need server-side rendering after switching from hash routing?
A: Not required, but recommended. Without SSR, Google must render your JavaScript to see filtered content. SSR provides complete HTML immediately, eliminating uncertainty about Google’s rendering process.

Q: Is the escaped fragment (#!) convention still valid?
A: No. Google deprecated escaped fragment handling in 2015. Don’t build new features around this obsolete convention. Modern sites should use standard URLs.


Summary

Hash fragments are client-side only and never sent to servers. Google can’t request or index URL variations based on hash parameters. Every hash variation appears as the same page to search engines.

Filter systems using hash parameters create SEO-invisible content. Valuable filtered views that could rank for specific long-tail searches are completely hidden from Google, regardless of how much valuable content they contain.

Convert to query parameters or path-based URLs for any content state that should be indexable. /properties?bedrooms=3 is indexable; /properties#bedrooms=3 is not. This typically requires refactoring your routing architecture.

Modern SPAs should use HTML5 History API instead of hash routing. This enables real URLs compatible with server-side rendering and search engine indexing while maintaining the smooth navigation experience of single-page applications.

Server-side rendering helps by providing complete HTML to Google immediately, rather than relying on Google’s JavaScript rendering to see filtered content. Next.js, Nuxt, and similar frameworks make SSR implementation straightforward.


Sources

  • Google Search Central: JavaScript SEO basics
  • Google Developers: AJAX crawling scheme deprecation
  • MDN Web Docs: History API
  • HTTP Specification: URI fragments