How to Do SEO for Single Page Applications (SPA)

On this page

A single-page application can fail at SEO when its content, and sometimes its URLs, only exist after JavaScript runs in a browser. Google does render JavaScript: its JavaScript SEO basics say it uses the rendered HTML to index pages. But everything an SPA shows depends on that render working: the bundle loading, the API answering, the router resolving the URL. The durable fix is to make every route that should rank return its real content and a correct status from the server, with server-side rendering or static generation, and to handle the SPA-specific failure points: routing, status codes, noindex, per-route titles and links.

“Google can render JavaScript” is true and misleading at the same time. It is true that Google executes your code. It is misleading if you take it to mean that everything your app does in a browser reaches the index.

Two failure modes, two tests

SPAs fail in two different ways, and each needs its own check.

Content not rendered. Run a live test in the URL Inspection tool and open the tested page. Compare Google’s screenshot and rendered HTML with what a user sees. A spinner, a skeleton or a missing main section means the content didn’t make it into Google’s render. That is a rendering failure.

URL not resolving. Turn JavaScript off in your browser and paste an interior URL directly into the address bar, loading it cold rather than clicking through the app. If the server has no route for it, you get an empty document or a 404 while the same URL works fine when the app’s router handles it. Check the status code in the browser’s network panel too: an empty page served with 200 and a 404 look alike on screen but are different problems. Either way, it is a routing failure.

You need both tests, because passing one says nothing about the other. A route can render perfectly when reached through the app and return nothing on a direct request.

The client-side routing trap

In a client-side-routed SPA, the server may only know the root path; the router takes over in the browser and makes interior URLs work there. Google, and anyone following a shared link, requests interior URLs from the server first. Every URL you want indexed should answer a direct request with a 200 status, and ideally with that route’s page in the server response.

The reverse case matters too: routes that don’t exist. The JavaScript SEO basics note that with client-side routing, using meaningful HTTP status codes can be impossible or impractical, so an app can answer a missing product with 200 and a “not found” view, which is a soft 404. The guide gives two ways out: redirect with JavaScript to a URL where the server returns 404, such as /not-found, or add a robots noindex meta tag to the error view with JavaScript.

Don’t make JavaScript undo a noindex

The same guide warns that Google may skip rendering when it meets a noindex tag, so a shell that ships with noindex and counts on the router to remove it can keep every real route out of the index. Add noindex only where a page should stay out.

Titles and descriptions. The JavaScript SEO basics say you can use JavaScript to set or change the meta description and the title element. A failure to look for is every route showing the same generic title from the base HTML because the per-route update never runs or runs late. With server rendering, emit each route’s own title, description and canonical in the response.

Links. Google can discover links only when they are <a> elements with an href attribute, and the guide recommends the History API for routing between views, not fragments. Navigation wired to click handlers with no href gives Google nothing to follow, so interior pages may never be found through your own links. Use real anchors and let the router enhance them.

Fix the foundation

In order of durability:

  • Server-side rendering. The server sends each route’s complete HTML on request.
  • Static generation. Routes whose content doesn’t change per request are built to HTML ahead of time.
  • Prerendering as a bridge. Generating static snapshots for routes can buy time while you move to server rendering or static generation.
  • Dynamic rendering, only as a stopgap. Google’s page on dynamic rendering calls it a workaround and not a recommended solution, and recommends server-side rendering, static rendering or hydration instead.

The framework you use to get there is an implementation detail. The goal is that every route that should rank has its content and a correct status in the server response.

Confirm the fix

After switching to server rendering or static generation, check rather than trust the configuration. With JavaScript off, load several route types directly, not only the home page, and confirm the main content, the route’s own title and the internal links are all in the raw HTML. A route can fall back to client rendering through a misconfiguration while the rest of the app works.

Then request indexing for your key routes in URL Inspection and confirm with a live test that the rendered page now carries the content. Google’s guide to asking Google to recrawl says “Crawling can take anywhere from a few days to a few weeks,” so pages Google previously saw as empty can be reassessed as they are recrawled.

Frequently asked questions

Can’t Google just render my JavaScript?

It does render it, and it uses the rendered HTML for indexing. The risk is everything the render depends on: scripts that load, APIs that answer, and routes the server can resolve. Put the content that needs to rank in the server response so it doesn’t depend on those.

My pages work when I click through the app. Why aren’t they indexed?

Check for a routing failure first. Load an interior URL cold, with JavaScript off, pasted into the address bar. If the server returns an empty page or a 404, Google gets the same thing on a direct request.

How should an SPA handle pages that don’t exist?

Google’s JavaScript SEO basics give two options for client-side routing: redirect with JavaScript to a URL that returns a real 404, or add a noindex robots meta tag to the error view with JavaScript. Either avoids serving a “not found” page with a 200 status.

Leave a comment

Your email address will not be published. Required fields are marked *