How to Make Your Single Page App Indexable by Google

SPAs are invisible to Google by default. Learn how to make your single page app indexable with SSR, prerendering, meta tags, sitemaps, and structured data.

How to Make Your Single Page App Indexable by Google

Your single page app works perfectly in the browser. But Google can't see it.

This is the most common problem in JavaScript SEO: SPAs render everything with JavaScript, and Google receives an empty HTML shell. Your app might look like a million bucks to users, but to Googlebot, it's a blank page with a script tag.

Here's how to fix it — regardless of which framework (React, Vue, Angular, Svelte) you're using.

Why Google Can't See Your SPA

When Googlebot visits a traditional website, it receives complete HTML — headings, paragraphs, links, meta tags. It can read and index the page immediately.

When Googlebot visits your SPA, it receives this:

<!doctype html>
<html>
<head><title>My App</title></head>
<body>
  <div id="app"></div>
  <script src="/bundle.js"></script>
</body>
</html>

All your content lives inside bundle.js. Google has to execute that JavaScript to see your content, and that process is:

  • Delayed — Google queues JavaScript pages for rendering, taking hours to weeks
  • Unreliable — complex apps with API calls, auth flows, or heavy state management often fail to render completely
  • Resource-limited — Google's renderer has a time and resource budget; if your app exceeds it, rendering stops mid-page

The result: Your pages either don't appear in Google, appear with missing content, or appear weeks after you deploy them.

The Fix: 5 Steps to Make Your SPA Indexable

Step 1: Add Server-Side Rendering

This is the only fix that actually solves the root problem. Everything else in this guide builds on it.

SSR means your server executes the JavaScript and sends fully rendered HTML to the browser (and to Googlebot). Instead of an empty div, Google receives your actual content.

Your options by framework:

Framework SSR Solution
React Next.js, React Router v7, or Remix
Vue Nuxt 3
Angular @angular/ssr (built-in since Angular 17)
Svelte SvelteKit

If you can't migrate to an SSR framework, you have two fallback options:

Option A: Dynamic rendering with a prerender service

Services like Prerender.io or Rendertron sit between your server and Googlebot. When Googlebot requests a page, the service renders the JavaScript and returns static HTML. Regular users still get the SPA experience.

User request    → Your SPA (normal)
Googlebot request → Prerender service → Static HTML snapshot

This is a band-aid — Google specifically recommends SSR over dynamic rendering. But it works if migration isn't possible right now.

Option B: Static Site Generation (SSG)

If your content doesn't change on every request (blog posts, product pages, landing pages), you can pre-render pages at build time:

# Most SSR frameworks support this
# Next.js: generateStaticParams
# Nuxt: nuxi generate
# Angular: ng build --prerender
# SvelteKit: export const prerender = true

SSG produces static HTML files that Google can read instantly — no JavaScript execution needed. The tradeoff is that content is only updated when you rebuild.

Verify SSR is working:

curl -s https://your-domain.com | head -50

If you see your page content (headings, text, links) in the raw HTML output, SSR is working. If you see an empty container div, it's not.

Step 2: Add Unique Meta Tags to Every Route

Once Google can see your content, you need to tell it what each page is about. Every route needs:

  • <title> — unique, descriptive, under 60 characters
  • <meta name="description"> — unique summary, 140-160 characters
  • <link rel="canonical"> — the definitive URL for this page

Why canonical URLs matter for SPAs: SPAs often have multiple URLs that serve the same content — /products and /products/, /products?sort=price and /products. Without a canonical tag, Google may index all of them as separate pages, splitting your ranking signals.

Example (framework-agnostic pattern):

<head>
  <title>Running Shoes for Beginners | Your Store</title>
  <meta name="description" content="Find the best running shoes for beginners. Compare cushioning, support, and price across top brands." />
  <link rel="canonical" href="https://your-domain.com/products/running-shoes" />
  <meta property="og:title" content="Running Shoes for Beginners | Your Store" />
  <meta property="og:description" content="Find the best running shoes for beginners." />
  <meta property="og:url" content="https://your-domain.com/products/running-shoes" />
  <meta property="og:image" content="https://your-domain.com/images/running-shoes.jpg" />
</head>

The key rule: Meta tags must be in the server-rendered HTML. If they're set by client-side JavaScript after page load, Google may not see them.

Step 3: Make All Navigation Crawlable

Googlebot discovers pages by following <a href="..."> links. It cannot:

  • Click buttons
  • Execute onClick handlers
  • Follow router.push() or router.navigate() calls
  • Process JavaScript-driven tab/accordion navigation

The fix is simple: Every link to another page in your app must be an <a> tag with an href attribute.

Before (invisible to Google):

<button onclick="navigateTo('/products')">Products</button>
<div @click="$router.push('/about')">About Us</div>
<span (click)="goTo('/contact')">Contact</span>

After (crawlable by Google):

<a href="/products">Products</a>
<a href="/about">About Us</a>
<a href="/contact">Contact</a>

Every SPA framework has a Link or RouterLink component that renders a proper <a> tag while maintaining the SPA navigation experience. Use it.

Audit your app: Search your codebase for navigate(, router.push, router.navigate, and onClick/(click)/@click handlers that trigger page changes. Replace them all with link components.

Step 4: Create and Submit an XML Sitemap

A sitemap is your insurance policy. Even if Google misses some of your links during crawling, the sitemap guarantees that Google knows about every URL you want indexed.

Basic sitemap structure:

<?xml version="1.0" encoding="UTF-8"?>
<urlset xmlns="http://www.sitemaps.org/schemas/sitemap/0.9">
  <url>
    <loc>https://your-domain.com/</loc>
    <lastmod>2026-03-30</lastmod>
    <priority>1.0</priority>
  </url>
  <url>
    <loc>https://your-domain.com/products</loc>
    <lastmod>2026-03-28</lastmod>
    <priority>0.9</priority>
  </url>
  <url>
    <loc>https://your-domain.com/about</loc>
    <lastmod>2026-01-15</lastmod>
    <priority>0.5</priority>
  </url>
</urlset>

Important rules:

  • Include every URL you want indexed — especially dynamically generated pages (product pages, blog posts)
  • Don't include URLs you've blocked in robots.txt or marked as noindex
  • Keep it updated — stale sitemaps with dead URLs hurt your crawl efficiency
  • Reference it in your robots.txt:
User-agent: *
Allow: /

Sitemap: https://your-domain.com/sitemap.xml

Submit it in Google Search Console under the Sitemaps section. Google will begin crawling the listed URLs within hours.

Step 5: Add JSON-LD Structured Data

Structured data qualifies your pages for rich results in Google Search — FAQ dropdowns, product cards with prices and ratings, how-to steps, breadcrumbs. Rich results get significantly more clicks than standard blue links.

Example — Article structured data:

<script type="application/ld+json">
{
  "@context": "https://schema.org",
  "@type": "Article",
  "headline": "How to Choose Running Shoes",
  "author": {
    "@type": "Person",
    "name": "Jane Smith"
  },
  "datePublished": "2026-03-30",
  "description": "A complete guide to choosing your first pair of running shoes.",
  "publisher": {
    "@type": "Organization",
    "name": "Your Store",
    "logo": {
      "@type": "ImageObject",
      "url": "https://your-domain.com/logo.png"
    }
  }
}
</script>

Example — FAQ structured data:

<script type="application/ld+json">
{
  "@context": "https://schema.org",
  "@type": "FAQPage",
  "mainEntity": [
    {
      "@type": "Question",
      "name": "How do I make my SPA indexable?",
      "acceptedAnswer": {
        "@type": "Answer",
        "text": "Add server-side rendering (SSR) so Google receives fully rendered HTML instead of an empty JavaScript shell."
      }
    }
  ]
}
</script>

Critical rule for SPAs: JSON-LD must be in the server-rendered HTML. If you inject it via JavaScript DOM manipulation (document.createElement('script')), it may not appear in the HTML that Google's crawler reads. Place it directly in your component templates.

Validate your structured data with Google's Rich Results Test after deployment.

How to Verify Your SPA Is Indexable

After implementing these fixes, verify everything is working:

1. Raw HTML check:

curl -s https://your-domain.com/your-page | head -100

You should see content, meta tags, and links in the output.

2. Google Search Console — URL Inspection: Paste your URL and click "Test Live URL." Google will show you exactly what it sees, including any rendering errors.

3. site: search:

site:your-domain.com

Shows all pages Google has indexed from your domain.

4. Structured data test: Run your URL through Google's Rich Results Test to validate JSON-LD.

Summary

SPAs are invisible to Google by default because they serve empty HTML shells. Here's the fix in priority order:

  1. Add SSR — the foundation. Google needs to see HTML, not an empty div.
  2. Set unique meta tags — title, description, and canonical on every route, in the server-rendered HTML.
  3. Use <a> links — Googlebot follows links, not click handlers.
  4. Submit a sitemap — tell Google about every URL explicitly.
  5. Add JSON-LD — qualify for rich results that increase click-through rates.

Step 1 is non-negotiable. If you can only do one thing, add server-side rendering.

Ready to check if Google can actually see your app? Scan What's Ranking to run a full visibility analysis.

FAQ

Can Google index JavaScript single page apps?

Google can execute JavaScript via its Web Rendering Service (WRS), but it's unreliable. WRS queues pages for deferred rendering, which can take hours to weeks, and complex SPAs frequently fail to render completely. Server-side rendering is the only reliable way to ensure Google indexes your SPA content.

Is SSR the only way to make a SPA indexable?

No, but it's the best way. Alternatives include prerendering/SSG (pre-building HTML at build time) and dynamic rendering (serving pre-rendered HTML to bots while serving the SPA to users). Google recommends SSR as the primary solution and considers dynamic rendering a workaround.

Does prerendering (SSG) work for SEO?

Yes, prerendering is excellent for SEO. It produces static HTML files that Google can read instantly — no JavaScript execution needed. The limitation is that content only updates when you rebuild. Use SSG for content that changes infrequently (blogs, landing pages) and SSR for dynamic content (search results, user-specific pages).

How do I know if Google can render my SPA?

Use Google Search Console's URL Inspection tool — click "Test Live URL" to see exactly what Google's renderer sees. You can also run curl -s https://your-site.com to see the raw HTML response. If your content isn't in the raw HTML, Google is depending on JavaScript rendering to see it.

My SPA uses hash routing (#/about). Is that a problem for SEO?

Yes. Googlebot ignores everything after the # in a URL — example.com/#/about and example.com/#/contact are both treated as example.com. Switch to HTML5 history mode (path-based routing like /about, /contact) so each page has a unique, crawlable URL.