← Back to Blog Start Free
Why Single-Page Apps Don't Get Indexed, and How to Fix It Without a Rewrite
Sep 30, 2026 · 12 min read single page app seo spa indexing javascript seo dynamic rendering pre-rendering googlebot

Why Single-Page Apps Don't Get Indexed, and How to Fix It Without a Rewrite

Single page apps often fail to get indexed because the initial HTML response contains little or no content, and crawlers may not execute JavaScript reliably or immediately. The fix is usually to serve pre-rendered or dynamically rendered HTML to crawlers while keeping the existing app for users, which avoids a full rewrite.

Quick Summary:

  • Googlebot processes JavaScript in a separate rendering wave after the initial crawl, which can delay indexing by hours or days depending on crawl budget.
  • A URL can be crawled and rendered but still excluded from the index if the rendered content is thin, duplicate, or blocked by robots.txt.
  • Dynamic rendering serves a pre-rendered HTML snapshot to crawlers while users get the normal client-side app, and it does not require changing application components.
  • Hash-based routing (URLs with #) is not crawlable as separate pages because fragments are not sent to the server.

If you built your product with a modern framework like React, Vue, or Svelte, you already know the appeal: fast interactions, a smooth user experience, and a clean separation between frontend and backend. But when you check Google Search Console a few weeks after launch, you might see a grand total of one indexed page: your homepage. Or worse, nothing at all. This is the classic single page app SEO problem, and it is almost never because Google hates your app. It is because Googlebot sees an empty shell where your content should be.

Table of Contents

Why Single-Page Apps Struggle With Indexing

Client-side rendering leaves the initial HTML shell empty of content

When a browser requests a client-side rendered SPA, the server responds with a minimal HTML file, often just a `<div id="root"></div>` and a script tag. The actual content, your product descriptions, your blog posts, your pricing details, only appears after the JavaScript bundle downloads, parses, and executes. For a human with a fast connection, this happens in milliseconds. For a crawler, it is a completely different story. The initial HTML response contains no meaningful text, no headings, no links. If a crawler does not execute JavaScript, it sees nothing.

Googlebot's two-wave indexing and the delay before JavaScript rendering

Googlebot does execute JavaScript, but not immediately. It uses a two-wave process: first it crawls the raw HTML, then later it queues the page for rendering. That second wave can happen hours or even days after the first crawl, depending on how much crawl budget Google allocates to your site. For a new site with few backlinks, that delay can stretch out. During that window, your page is crawled but not indexed, and if the rendered content never gets processed, it may never be indexed at all.

How routing that never triggers a full page load hides URLs from crawlers

In a traditional multi-page site, every link triggers a new HTTP request. In an SPA, clicking a link often just updates the URL via the History API and swaps out components without a server round-trip. That is great for speed, but it means crawlers never see a fresh HTML document for that route. If your navigation uses JavaScript-only click handlers instead of real `<a href>` tags, crawlers cannot discover those routes at all. They are invisible.

The difference between a page that renders and a page that gets indexed

Even if Googlebot renders your JavaScript and sees your content, that does not guarantee indexing. A URL can be crawled and rendered but still excluded from the index if the rendered content is thin, duplicate, or blocked by robots.txt. Google may also decide the content is not valuable enough to index. Rendering is a prerequisite, not a promise.

How to Diagnose Whether Your SPA Is Actually Indexed

Before you start fixing things, you need to know exactly what Google sees. Guessing wastes time.

Using the URL Inspection tool to see the rendered HTML Googlebot receives

The URL Inspection tool in Google Search Console shows the exact rendered HTML that Googlebot used, which is the fastest way to confirm whether content is visible to search. Paste in a URL, click "Test Live URL," and then view the "Rendered HTML" tab. If you see your content there, rendering is working. If you see an empty shell, you have your answer.

Checking server logs for Googlebot requests to your JavaScript bundles

Your server logs are a goldmine. Look for requests from Googlebot to your JavaScript files. If Googlebot is fetching your JS bundles, it is at least attempting to render. If it is not, it may be blocked by robots.txt or simply not prioritizing your site. You can also check whether Googlebot is requesting the individual routes of your app. If it only ever hits the root URL, your routing is likely the problem.

Comparing your sitemap URL count against the indexed page count in Search Console

Your sitemap should list every route you want indexed. In Search Console, under "Sitemaps," you can see how many URLs were submitted. Then go to "Pages" (formerly Coverage) and see how many are actually indexed. A large gap between submitted and indexed is a clear signal that something is wrong with rendering or content quality.

Testing with JavaScript disabled to see what a non-rendering crawler gets

Open your site in a browser with JavaScript disabled. What do you see? If it is a blank page or a loading spinner, that is exactly what a non-rendering crawler sees. Some crawlers, including certain social media bots and even some search engines, do not execute JavaScript. If your content is invisible to them, you are losing potential traffic and citations.

The Fix That Does Not Require a Rewrite

You do not need to throw away your SPA and rebuild it as a server-rendered monolith. There are several ways to give crawlers what they need while keeping your existing app for users.

Serving pre-rendered HTML to crawlers while keeping the SPA for users

Pre-rendering means generating a static HTML file for each route at build time. When a crawler requests a URL, it gets a fully populated HTML page. When a user requests the same URL, they get the normal SPA experience. This is often the simplest fix because it does not require changing your application code. Tools like prerender.io or custom build scripts can handle this.

Dynamic rendering as a bridge, and when it is the right tradeoff

Dynamic rendering detects whether the request is from a crawler or a user. If it is a crawler, it serves a pre-rendered HTML snapshot. If it is a user, it serves the normal client-side app. This is a bridge, not a permanent solution, but it works well when you need to fix indexing quickly without refactoring your entire frontend. The tradeoff is that you need to keep the pre-rendered snapshots up to date with your app's content.

Static pre-rendering at build time for routes that do not change often

If your app has routes that do not change frequently, like a homepage, about page, or blog index, you can pre-render them at build time. This is the most performant option because the HTML is ready to serve immediately. It also reduces the load on your servers. For routes that change often, like a user dashboard, you can skip pre-rendering or use dynamic rendering.

How a rendering layer sits in front of your existing app without touching components

The key insight is that you can add a rendering layer without modifying your components. This layer sits between the crawler and your app. It intercepts requests, checks if the requester is a crawler, and if so, serves a pre-rendered version. Your app code remains untouched. This is how most modern pre-rendering services work, and it is why you do not need a rewrite.

What to check in any tool that promises to solve this: does it serve real HTML, does it update on deploy, does it handle your routing

When evaluating a pre-rendering or dynamic rendering tool, ask three questions. First, does it serve real HTML that contains your content, not just a meta refresh or a JavaScript redirect? Second, does it automatically update when you deploy new content? Third, does it correctly handle your routing, including nested routes and dynamic parameters? If the answer to any of these is no, keep looking.

This is also where an app like AlmightyFormulaSEO fits into a broader workflow. It handles the content side of the equation by generating GEO-optimised articles and publishing them to your blog on a schedule you approve, which means you have something worth indexing in the first place. The rendering fix gets crawlers to see your content; the content itself still has to be there.

Technical Fixes You Can Ship This Week

These are changes you can make today, without waiting for a larger refactor.

Replace `<div onClick={...}>` with `<a href="/about">`. This is the single most impactful fix for crawlability. Real links are discoverable by crawlers and pass link equity. If you need to prevent a full page reload, you can intercept the click with JavaScript after the href is in place.

Generating a sitemap that lists every route, not just the root URL

Your sitemap should include every route you want indexed. If you have a blog with 50 posts, your sitemap should list all 50 URLs plus your main pages. An XML sitemap generator can help, but make sure it reflects your actual routes. Submit it in Search Console and monitor the indexed count.

Setting canonical tags correctly when the same content is reachable at multiple URLs

If your app can serve the same content at multiple URLs (e.g., with and without trailing slashes, or with query parameters), use canonical tags to tell Google which URL is the preferred one. This prevents duplicate content issues that can dilute your ranking.

Using history API routing instead of hash routing so URLs are crawlable

Hash-based routing (URLs with #) is not crawlable as separate pages because fragments are not sent to the server. Switch to History API routing (e.g., `/about` instead of `/#/about`). This requires server configuration to serve your index.html for all routes, but it is a one-time setup.

Avoiding lazy-loaded content that only appears after user interaction

If your content only loads when a user clicks a tab or scrolls, crawlers may never see it. Either load that content by default or ensure it is present in the initial HTML. You can still use lazy loading for images and other media, but critical text content should be immediately available.

What to Expect After You Fix Rendering

Indexing is not instant: crawl budget and reprocessing time

Once you fix rendering, Google needs to recrawl your pages. This can take days or weeks, depending on your site's crawl budget. You can speed things up by requesting indexing for priority URLs in Search Console.

How to request re-indexing for priority URLs

In the URL Inspection tool, after testing a live URL, you can click "Request Indexing." Do this for your most important pages first. There is a limit to how many you can request per day, so prioritize.

Monitoring coverage reports for soft 404s and duplicate content

After re-indexing, check the Pages report for issues like soft 404s (pages that return a 200 but have no content) or duplicate content. These can prevent indexing even if rendering is fixed.

Why some pages may still not index and what that usually means

If a page still does not index after all this, it usually means Google has decided the content is not valuable enough, or it is too similar to other pages on your site. In that case, focus on improving content quality and uniqueness.

Frequently Asked Questions

Can a single page app rank in Google without server-side rendering?

Yes, but it is harder. Googlebot can render JavaScript, but the delay and potential for errors make it unreliable. You can rank with client-side rendering if your content is high quality and your site has strong signals, but pre-rendering or dynamic rendering gives you a much better chance.

What is the difference between dynamic rendering and server-side rendering?

Dynamic rendering serves a pre-rendered HTML snapshot to crawlers while users get the client-side app. Server-side rendering generates the full HTML on the server for every request, for both users and crawlers. Dynamic rendering is a bridge; SSR is a more permanent architectural choice.

How long does it take for Google to index a single page app?

It varies. For a new site, it can take weeks. For an established site with good crawl budget, it might be days. The two-wave indexing process means even after crawling, rendering can be delayed.

Do I need to rewrite my app to fix SEO?

No. You can fix most SPA SEO issues with pre-rendering, dynamic rendering, or technical fixes like real href links and sitemaps. A full rewrite is rarely necessary.

Does Googlebot execute JavaScript?

Yes, Googlebot executes JavaScript, but it does so in a separate rendering wave that can be delayed. It is not guaranteed to execute all JavaScript perfectly, especially if your code is complex or relies on unsupported APIs.

What is the fastest fix for a single page app that is not indexed?

The fastest fix is usually to add real href links in your navigation and submit a sitemap. If that does not work, implement dynamic rendering to serve pre-rendered HTML to crawlers. Both can be done without a rewrite.

Conclusion

Fixing single page app SEO does not require a rewrite. It requires understanding how crawlers see your app and giving them a version they can index. Start with the diagnosis: use the URL Inspection tool to see what Googlebot actually gets. Then apply the fixes that make sense for your stack: real links, a complete sitemap, history API routing, and a rendering layer that serves pre-rendered HTML to crawlers. Most of these can be shipped this week. The rest, like dynamic rendering, can be set up without touching your components. Once rendering is fixed, focus on content quality and monitoring. Indexing will follow.

If you want to focus on building your product while an autonomous engine handles the content side, AlmightyFormulaSEO is free during early access and lets you approve every article before it publishes.

Want this done for you, automatically?

AlmightyFormulaSEO researches, writes and publishes GEO-ready articles for your site - you approve, it ships.

Start Free