Single-page application SEO: make each view indexable

Single-page application SEO needs real URLs, plain HTML links and content Google can render. These are the steps, drawn from Google's own documentation.

SEOAgent team · · 7 min read

A single-page application (SPA) loads one web document and then swaps in new content with JavaScript, which can bring performance gains and a more dynamic experience. MDN's glossary entry for SPA lists SEO among the trade-offs of that design. This guide covers single-page application SEO from Googlebot's side: how Google crawls, renders and indexes these apps, and the steps that get each view indexed.

Single-page application SEO means that every view has its own URL, a regular HTML link points to each one, and its content appears in the page Google renders. Googlebot fetches the URL, queues the page for rendering, runs the JavaScript, and indexes the rendered HTML. Server-side or pre-rendering is still a great idea, since not all bots run JavaScript.

Prerequisites

  • A Search Console property with verified ownership. You use it for the Sitemaps report and the URL Inspection Tool. Google's help page on verified ownership covers the setup.
  • Server settings you can change. You need a URL such as /not-found that returns a 404 status. Step 3 depends on it.
  • Access to the head of each view. You need to set its title, meta description and canonical link, either in the HTML or with JavaScript.
  • A list of your URLs. Step 5 builds the sitemap from it.

Step 1: Give each view its own URL

Google can only discover links that are regular HTML links with an href attribute, so a route that exists only in a click handler gives Googlebot nothing to follow. Use the History API so that each view has an address that loads on its own.

<!-- Googlebot can't reliably resolve this URL -->
<a href="#/products">Our products</a>

<!-- Googlebot can follow this link -->
<a href="/products">Our products</a>

When the view changes, call window.history.pushState so the address bar and the browser history both show the new URL. Google's Building Indexable Progressive Web Apps post uses a product page as its example: a URL such as https://www.example.com/product/25/ should deep link to that product.

Step 2: Make sure Google can render your content

Google processes a JavaScript app in three phases: crawling, rendering and indexing. Googlebot reads robots.txt before it fetches a URL. Google won't render JavaScript from blocked files or on blocked pages, so don't block the scripts your app needs.

Once Google's resources allow, a headless Chromium renders the page. The page can wait in that queue for a few seconds, and sometimes longer. Googlebot then parses the rendered HTML for links again, so a link that exists only after rendering is found on that second pass.

Some apps use the app shell model, where the first HTML has no real content, so Google has to run the JavaScript before it can see the page. Content in the first response doesn't have to wait for rendering. Server-side rendering sends content that way for every request. Hybrid rendering does it for a direct visit to a URL and leaves later navigation to the client, which Google's 2016 post calls the modern approach.

Primary content must appear without a click, swipe or typing, because Google won't load content that needs those. Google's lazy-loading guide covers the search-friendly way to lazy-load content. Finally, open a page in the URL Inspection Tool and read its rendered HTML, because content missing from that output won't be indexed.

Step 3: Return the right status for missing views

Googlebot uses HTTP status codes to find out whether something went wrong. A 404 tells it that a page could not be found, and a 401 tells it that a page sits behind a login. In a client-side app, routing happens in the browser, so meaningful status codes can be impossible or impractical to return for each view. Google's guide suggests two fixes for error views.

The first is a JavaScript redirect to a URL that returns a 404 status, such as /not-found:

fetch(`/api/products/${productId}`)
  .then(response => response.json())
  .then(product => {
    if (product.exists) {
      showProductDetails(product); // shows the product information on the page
    } else {
      // this product does not exist, so this is an error page.
      window.location.href = '/not-found'; // redirect to 404 page on the server.
    }
  });

The second adds a robots meta tag to the error view with JavaScript. The tag it adds looks like this:

<meta name="robots" content="noindex">

Google's sample assumes the HTML has no other robots meta tag, so check that before you copy it.

Step 4: Set each view's title, description and canonical

Each view needs a unique, descriptive title element and meta description, since they help people pick the right result. JavaScript can set or change both, so your router can update them whenever the view changes.

The canonical link tag tells Google which URL is the canonical version of a page. Put it in the HTML whenever you can:

<link rel="canonical" href="https://www.example.com/product/25/" />

If the HTML can't carry it, inject it with JavaScript and leave it out of the original HTML. Google picks up an injected canonical while it renders the page. If the original HTML already has one, JavaScript must set the same URL.

SEOAgent's website audit checks each page's status code, title, meta description, H1 and canonical, then flags duplicate titles and descriptions across the site.

If your app writes JSON-LD with JavaScript, Google's guide to generating structured data with JavaScript covers injecting it and testing the result.

Step 5: Submit a sitemap that lists every view

A sitemap tells Google which URLs you want in search results, so list each view at its absolute address, such as https://www.example.com/product/25/. Google generally shows canonical URLs in results, so list the canonical version of each view.

<?xml version="1.0" encoding="UTF-8"?>
<urlset xmlns="http://www.sitemaps.org/schemas/sitemap/0.9">
  <url>
    <loc>https://www.example.com/foo.html</loc>
    <lastmod>2022-06-04</lastmod>
  </url>
</urlset>

Each sitemap holds up to 50MB uncompressed or 50,000 URLs. Split a bigger list into several sitemaps, and optionally group them with a sitemap index file. Google ignores priority and changefreq, and it uses lastmod only when the dates are consistently and verifiably accurate.

For a large app, have the server build the sitemap from its data, because a hand-written file is tedious to keep current.

Submit the sitemap in the Sitemaps report in Search Console, or add a line like this to your robots.txt file:

Sitemap: https://example.com/my_sitemap.xml

Submitting a sitemap is only a hint. It doesn't guarantee that Google downloads the file or crawls every URL in it. Googlebot finds new URLs through href links, so link each view from somewhere on the site as well. After you submit, the Sitemaps report shows when Googlebot accessed the file and any processing errors.

Common mistakes in single-page application SEO

  • Fragment and hash-bang routes. Routes such as #/products give Googlebot URLs it can't reliably resolve. The #! structure is discouraged too, since Google's 2016 post says the AJAX crawling scheme that used it is no longer recommended.
  • Deep links sent home. A deep link to a page that exists shouldn't redirect to the home page.
  • Different content for Google. Don't show Google content that differs from what users see.
  • Stale cached files. Googlebot caches aggressively, and the rendering service may ignore caching headers, so it can run outdated JavaScript or CSS. Fingerprinted file names, such as main.2bb85551.js, avoid the problem.
  • Development blocks left on. Google's 2016 post suggests keeping crawlers out of a site under development, and it warns you to unblock them at launch.

What's new (as of 2026-10-03)

Google's Understand the JavaScript SEO basics page, which this guide follows, was last updated on 2026-03-04 UTC. The sitemap guide, Build and submit a sitemap, was last updated on 2026-07-08 UTC. Neither date says what changed, so check the page before you rely on one of its details. Google's Building Indexable Progressive Web Apps post is older, dated 9 November 2016, so read its URL advice as background.

Once every view is crawlable and indexed, the next question is whether AI answers cite those pages, and SEOAgent's post on Google AI Mode and how to get cited covers that.

Key takeaways

  • Google renders JavaScript after crawling and indexes the rendered HTML, so content missing from that output can't be indexed.
  • Googlebot only discovers links that are regular HTML links with an href attribute, and it can't reliably resolve fragment routes such as #/products.
  • For error views in a client-side app, Google's guide suggests a JavaScript redirect to a URL that returns a 404, or a noindex robots meta tag added with JavaScript.
  • Put the canonical link in the HTML when you can, and never point a JavaScript canonical at a different URL than the original HTML does.
  • Google won't load primary content that needs a click, swipe or typing, so show it without interaction.

FAQ

Does Google index single-page applications?

Yes, as long as the content is in the rendered HTML. Googlebot queues the page for rendering and uses the rendered HTML to index it, so anything missing from that output won't be indexed.

Do single-page applications need server-side rendering for SEO?

Not necessarily. Google still calls server-side or pre-rendering a great idea, because it makes pages faster for users and crawlers, and not all bots run JavaScript. If you use client-side rendering, Google's 2016 progressive web app post recommends checking that your content renders for its crawler.

How can I see what Google renders for my single-page application?

Use the URL Inspection Tool in Search Console and look at the rendered HTML, because Google uses that HTML to index the page. Google's JavaScript SEO guide also names the Rich Results Test for checking the rendered HTML.

How should I handle error pages in a single-page application?

When meaningful status codes aren't practical for each view, Google's guide suggests a JavaScript redirect to a URL that does return a 404, such as /not-found, or a robots noindex meta tag added to the error view with JavaScript. The redirect option needs that URL to return a real 404 status, so confirm it in your server settings.

More from the blog

See which searches your site can win

Add your domain and the first research run lists them. That first run uses no credits.