Skip to content
Searchpedia SEO field notes Callum Bennett Callum

Site ops

Pushstate SEO

If you are building a single-page app, stop using hash fragments and use pushState instead, but do not assume that alone makes your content crawlable.

Beginner3 min readUpdated 2026-07-27Notes by Callum Bennett

Start here

  • Use pushState to replace hash fragments in single-page apps, giving each content state a unique, clean URL.
  • Ensure the server returns the correct HTML for each pushed URL, or use server-side rendering to serve that content.
  • Pair pushState with real <a> tags pointing to the same URLs so Google can discover them via links.
  • Test your pushed URLs with Google’s URL Inspection Tool to confirm they are rendered and indexable.
  • Include a fallback for browsers that do not support the History API, such as hash-based navigation.

Plain-English take

pushState is a browser API that lets you change the URL in the address bar without reloading the page. Think of it like this: you click a link in a single-page app, the content changes, and the URL updates to something like /products/blue-widget — but the page never actually refreshes. That is pushState. It pushes a new entry onto the browser’s history stack, so the back and forward buttons work as expected. For SEO, this matters because search engines need real URLs to index each distinct content state. If you rely on hash fragments (#!), you are making Google’s job harder. I prefer pushState over hashes every time. But here is the catch: pushState only changes the URL. It does not by itself make the content visible to a crawler. If the server returns the same HTML for every pushed URL, Google will see duplicate content. I have seen plenty of SPAs where the URL changes but the underlying page source is a single shell. That is a waste of a good API. You need to ensure that each pushed URL resolves to unique, crawlable content — either through server-side rendering, pre-rendering, or dynamic rendering. I once worked on a site with 500 product variants, each with a pushState URL, but the server returned the same index.html for all of them. Google indexed only the first one. That is the reality.

When it actually matters

pushState becomes critical when you have a JavaScript-heavy site where the user sees different content without a full page load. Typical scenarios: e-commerce category filters, infinite scroll, tabbed interfaces, single-page app navigations. Google’s crawler can execute JavaScript, but it does not click every button. If you rely on click events to load content and update the URL via pushState, Google may never trigger those events unless you provide real links. The best practice is to pair pushState with actual <a> tags that point to the same URLs. That way, Google can discover the URLs via links, and when it renders the page, pushState keeps the URL in sync. I have seen a [JavaScript SEO](/javascript-seo/) audit where the client used pushState but no internal links — Google crawled zero of those pages. That is a mistake. Another case: [pagination](/pagination/) with infinite scroll. Each time the user scrolls, new content loads and the URL updates via pushState. But if you do not provide a canonical URL or a [sitemap](/sitemap/), Google may treat those as separate pages with thin content. I recommend using pushState for each distinct content state that deserves a URL, but only if you can also serve that content without JavaScript. For instance, if you have a [canonical tag](/canonical-tag/) pointing to the first page, you are telling Google to ignore the others. That defeats the purpose. pushState matters most when the URL represents a unique, indexable resource. For simple websites where every page is a full server-rendered HTML, you do not need pushState at all.

What I got wrong

I used to believe that simply using pushState was enough to make a single-page app SEO-friendly. I thought that because the URL changed, Google would automatically pick up the new content. I was wrong. The URL is just a label. The content must be there when the crawler requests that URL. I learned this the hard way on a project where I built a dashboard with multiple views. I used pushState to update the URL between /dashboard/overview, /dashboard/reports, etc. The server always returned the same HTML shell. Google indexed only /dashboard/ and treated the rest as duplicates. I had to add server-side rendering for those paths. That took weeks. Another mistake: I used pushState without checking if the browser supported it. It is widely supported, but I still encountered issues with older browsers. I now always include a fallback to hash-based navigation or a full page reload. I also assumed that pushState would automatically handle the back button properly. It does, but only if you also listen to the popstate event and update the content accordingly. I have seen sites where the URL changes but the content does not on back navigation. That is a bad user experience. So my advice: test every pushState URL manually in a browser, with JavaScript disabled, and with Google’s URL Inspection Tool. If the content is not there, pushState is just window dressing. [Duplicate content](/duplicate-content/) warnings from Search Console are a clear sign you have not set this up correctly.

Next step

Quick answers

Does pushState affect the browser's back button?

Yes, pushState adds a new entry to the history stack, so when the user clicks back, the previous URL is restored. You must listen for the popstate event to update the content accordingly.

How is pushState different from replaceState?

pushState creates a new history entry, allowing the user to navigate back to the previous state. replaceState modifies the current entry without adding a new one. Use replaceState for updates that should not be separate history entries, like sorting.

Do I need to use pushState if my site is server-rendered?

No. If each page is a full HTML document served from the server, pushState is unnecessary. It is only useful for client-side navigation where you want to avoid full page reloads.

Sources

Primary documentation is linked directly. Anything commercial is marked nofollow.

Notes from Callum Bennett.