Single Page App SEO: Why 10–20% of Your Routes Must Be Server First

•15 min read
Single Page App SEO: Why 10–20% of Your Routes Must Be Server First

Yes, SPAs can rank, but only when every route you want indexed returns crawlable HTML, correct metadata, and the right status code directly from the server. Single page app SEO is not a plugin or a settings toggle. It’s a per-route architecture decision: classify each URL as public or private, then choose SSR, SSG, or CSR based on what that specific route needs to do.


TL;DR:

  • Routes that rely solely on client-side rendering with no initial HTML content risk being invisible to most crawlers, especially AI search engines.
  • Using hash-based routing prevents search engines from indexing different views, whereas history-based routing with clean URLs ensures better crawlability.
  • Public routes requiring ranking should use SSR, SSG, or incremental rendering, while private, gated pages remain best suited for client-side rendering.
  • Proper server configurations are crucial for returning accurate status codes, canonical URLs, and correctly reflecting route existence in sitemaps to optimize crawl efficiency.
  • Regular automated checks of raw server responses, sitemap consistency, and metadata presentation help identify regressions that can harm search rankings.

Table of Contents

Single Page App SEO Starts With Route Classification

Most SPA SEO failures trace back to one bad assumption: that the same rendering strategy works for every route in the app. It doesn’t. A pricing page and a logged-in billing dashboard have completely different indexing needs, yet teams often ship both with identical client-side rendering and wonder why only one of them shows up in search results.

The initial HTML document is where the damage happens first. When a crawler requests a React or Vue SPA route and gets back a near-empty <div id="root"></div>, it has nothing to index until JavaScript executes. Googlebot will often run that JavaScript on a second pass, but that pass isn’t guaranteed, isn’t instant, and isn’t how every crawler behaves. AI search crawlers are less forgiving: they may miss 50 to 80 percent of content that isn’t present in the initial HTML document, because many of them skip JavaScript execution entirely. If your product pages depend on client-side rendering to populate titles, descriptions, and body copy, you’re invisible to a growing share of the tools people now used to find businesses.

Routing architecture compounds the problem. Older SPA setups relied on hash-based routing (example.com/#/pricing), where the fragment after the # is treated as a page anchor, not a distinct URL. Search engines generally can’t index separate views built this way. History-based routing, using the browser’s History API, gives every view its own crawlable path (example.com/pricing), and it’s the documented fix for this exact issue.

A handful of other failure modes show up repeatedly in SPA audits:

  • Client-only metadata injection. Titles and meta descriptions get set with document.title or a JavaScript-only head manager, so the server response ships a generic or blank <title> tag.
  • Soft 404s. A missing route resolves client-side and returns an HTTP 200 status with a “Not Found” message rendered in JavaScript, telling search engines the page exists and is fine.
  • JS-only navigation. Internal links built as onClick handlers with no real <a href> attribute, so crawlers that don’t execute JavaScript never discover the linked pages at all.
  • Render-blocking data fetches. Content that depends on a client-side API call after mount, leaving a loading spinner in the HTML a crawler actually receives.

Each of these is fixable without abandoning your framework. The fix is almost always the same: move the decision about what gets rendered, and when, from the browser to the server.

Which Rendering Strategy Fits Which Route?

Once you’ve split your routes into public and private, the rendering choice mostly makes itself. The mistake most teams make is picking one rendering model for the whole app instead of matching the model to the route.

Static Site Generation (SSG) builds the full HTML for a route at build time. It’s the right call for content that doesn’t change per request: blog posts, marketing pages, pricing pages, documentation. There’s no server compute cost at request time, response times are close to instant, and the HTML a crawler sees is identical to what a browser renders. The trade-off is freshness. If the content changes, you need a new build, which is a non-issue for a blog post and a real constraint for anything tied to inventory or live pricing.

Hands arranging site build wireframes on desk

Server-Side Rendering (SSR) generates HTML on the server for every request. This is the right fit for public routes with frequently changing data, like a product catalog page where availability shifts hourly, or a search results page generated from a live query. The cost is server compute on every hit, which is why SSR routes almost always need a caching layer sitting in front of them. Skip the cache and you’ll find your SSR routes are the slowest pages on the site.

Hands plugging network cable into server rack

Incremental Static Regeneration and Partial Prerendering split the difference. ISR serves a static page but revalidates it in the background after a set interval, so you get SSG speed with periodic freshness. Partial Prerendering (PPR) goes further, shipping a static shell immediately while streaming in dynamic sections, which means a page can be crawler-friendly and still show live data without a full server round trip for the whole page. This is the option to reach for when you have hundreds of product or listing pages and full SSR would be too expensive to run at scale.

Selective prerendering solves a narrower but common problem: you’re committed to a client-side SPA architecture, but you have a handful of routes (homepage, key landing pages, top blog posts) that absolutely need to be indexed well. React Router’s tooling lets teams prerender specific high-value routes at build time and fall back to standard SPA behavior for everything else. It’s not a full architectural rewrite. It’s a targeted patch that gets your highest-traffic pages into the index while your app shell continues to behave like a normal SPA everywhere else.

Client-Side Rendering (CSR) is still the correct choice for routes nobody should be searching for in the first place: authenticated dashboards, account settings, in-app workflows, anything gated behind a login. There’s no SEO value in indexing a user’s private data view, so the render-on-the-client cost is irrelevant there. The error isn’t using CSR. It’s using CSR on a public marketing page and calling it a rendering strategy.

Here’s how that maps out in practice:

Rendering approach Best for Watch out for
SSG Blog posts, docs, marketing pages Rebuild required for content updates
SSR Live catalogs, search results, personalized public pages Server cost, needs a caching layer
ISR / PPR High-volume listing pages at scale Added build/deploy complexity
Selective prerender + SPA fallback Apps that can’t fully migrate off CSR Only covers the routes you explicitly prerender
CSR Authenticated dashboards, private app states Wrong choice for anything public you want ranked

Pro Tip: Audit your route list before you touch rendering code. Sort every route into “should be indexed” and “should never be indexed.” That fifteen-minute exercise usually reveals that only 10 to 20 percent of your app actually needs server-first rendering, which makes the migration far smaller than it initially looks.

Dynamic rendering, serving different HTML to bots than to human visitors, gets pitched as a shortcut around all of this, but it’s a fragile one. It’s being phased out as a recommended long-term fix because it requires bot detection to stay accurate forever, and any mismatch reads as cloaking. Server-first rendering for indexable routes avoids the problem entirely by making sure everyone, human or bot, gets the same HTML.

Getting Routing and Sitemaps Right

Rendering strategy only matters if crawlers can actually find and understand your URLs. That depends on three things: how your router builds paths, how those paths get canonicalized, and whether your sitemap reflects reality.

  1. Switch to history-based routing for every indexable view. If you’re still shipping hash routes for public pages, this is the first fix, not an optional one. BrowserRouter in React Router or the equivalent in your framework gives each view a real, distinct URL that crawlers treat as a separate page rather than an anchor jump on the same page.
  2. Give every indexable view a stable URL and a self-referencing canonical tag. A route that can render at /products/shoes and /products/shoes?ref=nav needs a canonical pointing to the clean version, or you’ll split ranking signals across near-duplicate URLs.
  3. Kill infinite URL generation before it starts. Faceted navigation, filter combinations, and sort parameters can generate thousands of unique URLs from a handful of real pages. Canonicalize filtered views back to the base category page, or add a noindex tag to combinations that offer no unique search value.
  4. Generate your sitemap from the same route manifest your router uses, not as a hand-maintained side file. When the sitemap and the router pull from one source of truth, a new route automatically appears in the sitemap and a removed route automatically drops out, which is exactly the kind of drift that causes stale or missing sitemap entries in apps that maintain the two separately.

The sitemap-drift problem deserves more attention than it usually gets. Teams add a new route, ship it, and forget the sitemap exists as a separate artifact. Six months later, a third of the sitemap points to routes that were renamed or removed, and the newest quarter of the app isn’t listed at all. Automating sitemap generation from the router configuration, rather than treating it as documentation someone updates manually, removes this failure mode permanently.

Server-Rendered Metadata Is Not Optional

Metadata that lives only in the DOM after JavaScript runs is metadata that many crawlers never see. Every indexable route needs its title tag, meta description, canonical URL, robots directive, and Open Graph tags present in the raw HTML the server sends back, before any script executes.

The practical fix is tying metadata generation to the same data-fetching layer that populates the page content, rather than treating it as a separate client-side step. React Router’s loader pattern, paired with a meta export, lets you compute page metadata from the same route data and render it into the initial response. Next.js offers an equivalent with its route-based metadata API. Both approaches mean the title tag for a product page is generated from the same product data that renders the page, in the same server pass, instead of being set afterward with a useEffect hook that a crawler might never trigger.

Key elements to lock into the server response for every public route:

  • Unique <title> and <meta name="description"> tags per route, not a single static tag reused across the whole app.
  • A self-referencing <link rel="canonical"> tag.
  • Open Graph and Twitter Card tags for any route likely to get shared or linked externally.
  • Schema.org JSON-LD markup where it applies (Product, Article, LocalBusiness, FAQPage), rendered server-side, not injected after mount.

A detail that trips up otherwise-solid implementations: a noindex robots tag accidentally left in the app shell template. If that tag was added during development to keep a staging build out of search results and never removed, every route inheriting that shell layout gets silently deindexed, no errors, no warnings, just a slow bleed of pages disappearing from search over a few weeks. This is one of the more common causes of an SPA that “used to rank fine” losing its entire index footprint at once. Check your shell template’s head tags whenever a broad ranking drop shows up with no obvious cause.

Status Codes and Crawl Hygiene Matter More Than Teams Think

A missing page that returns HTTP 200 is worse for your search performance than a missing page that returns 404. Soft 404s, where a client-rendered “Page Not Found” message displays but the server response still says 200 OK, tell search engines the page is valid content. That confuses ranking systems and wastes crawl budget on URLs that shouldn’t exist in the index at all.

Fixing this requires server involvement, not just client-side routing logic:

  • Configure your server or edge function to return an actual HTTP 404 (or 410 for permanently removed content) when a requested route doesn’t exist, rather than letting the client-side router catch it and render a “not found” component under a 200 status.
  • Use server-side redirects (301 for permanent moves, 302 for temporary) instead of client-side router.push() calls when a URL has changed. A JavaScript redirect that fires after the page loads still serves a 200 for a URL you’ve abandoned.
  • Prune sitemap entries for routes that no longer exist or that never should have been indexable, and apply robots.txt disallow rules to sections of the app that generate low-value URLs at scale (internal search results pages are a common offender).

Crawl budget is finite even for well-resourced sites. Every request a crawler spends on a soft 404 or an orphaned filter combination is a request it didn’t spend discovering your actual content. Getting status codes right isn’t a compliance checkbox. It’s how you make sure the pages you care about get crawled on a reasonable schedule.

Testing and CI Checks to Validate Indexability

Manual spot-checks catch obvious breaks. They miss the slow regressions, like a metadata field that silently stopped rendering after a refactor three sprints ago. Build these checks into a routine, and into CI, so they run without anyone remembering to do it.

  1. Fetch the raw server response with curl. Run curl --location --user-agent "Mozilla/5.0" against a route and grep the output for your <h1>, <title>, and meta description tags. If they’re not in that raw response, they’re not in what most crawlers see on the first pass.
  2. Compare view-source against rendered HTML in Google Search Console. The URL Inspection tool shows both the raw HTML Google fetched and the HTML after Google’s renderer runs JavaScript. A gap between the two, content present in “rendered” but missing from the raw fetch, is your indexability risk map.
  3. Test with non-JS fetches and AI-bot user agents. Many AI answer engines fetch pages without executing JavaScript at all. Running the same curl test with a few different user-agent strings tells you whether the content you want cited in an AI-generated answer actually exists in the document those systems receive.
  4. Add automated assertions to your deploy pipeline. A CI step that fails the build if a route is missing its title tag, returns the wrong status code, or ships an empty meta description catches regressions before they reach production, not three weeks after a traffic drop shows up in analytics.

Pro Tip: Keep a running checklist of your five highest-traffic public routes and run the curl and Search Console checks against them after every deploy touching routing or data-fetching code. Most SEO regressions in SPAs come from a shared component change, not a page-specific one, so the routes that broke are rarely the ones the developer was actually working on. A structured audit checklist is worth keeping alongside this as a broader reference.

Deciding Between SPA and Server-First Architecture

Not every app needs a rewrite. The decision comes down to three questions: how many public URLs need to rank, how often that content changes, and how much engineering time you actually have to maintain a hybrid rendering setup.

A useful rule of thumb: marketing pages, documentation, blog content, and product or service pages belong on SSG or SSR. Authenticated dashboards, in-app workflows, and account settings belong on CSR, because no one is searching for them and indexing them would be a privacy problem, not a growth opportunity.

  • If your public-facing footprint is under a dozen routes, selective prerendering with an SPA fallback is often enough. A full framework migration isn’t worth the engineering cost for that scale.
  • If you’re generating hundreds of public listing or product pages, ISR or PPR usually pays for itself in both crawl efficiency and server cost compared to full SSR on every request.
  • If your team is small and ships fast, weigh the ongoing maintenance cost of a hybrid rendering setup against just moving public routes to a framework built around server-first rendering by default.
  • Personalization requirements on a “public” route (logged-in pricing tiers, localized content) push you toward SSR with caching, not static generation.

What Actually Separates SPAs That Rank From Ones That Don’t

Most write-ups on single page app SEO treat it as a checklist problem: add meta tags, fix the sitemap, done. The pattern we see building custom-coded sites for service businesses is different. The SPAs that struggle almost always have the rendering decision made once, at the framework level, for the entire app. The ones that rank made that decision per route, deliberately, and revisited it as the app grew.

That’s the discipline behind how Forefront Industries approaches custom web development: classify the route, decide what a crawler needs to see in the first response, then build the rendering layer to match. It’s a habit carried over from enterprise CRM and lifecycle marketing work for brands including Ring, Blink, and Audible during in-house enterprise work at Amazon, where a broken data pipeline doesn’t just hurt a ranking, it drops a lead silently and nobody notices for weeks. That same route-by-route rigor is what’s behind the 30 percent improvement in inbound lead quality clients have seen after a rebuild.

The uncomfortable truth for a lot of engineering teams is that the fix usually isn’t a new library. It’s going back through the route table and being honest about which pages actually need to rank, then giving those pages the server-side treatment they’ve been missing.

- Jeremy

Get a Technical SEO Audit Built for Your Route Architecture

Forefront Industries is the alternative to guessing which routes are costing you rankings: a technical audit that maps every public route in your app, flags where server-rendered HTML, metadata, or status codes are missing, and hands you a fix list ranked by impact, not a generic score.

Forefront Industries

The engagement path is straightforward. Start with a free website audit that checks rendering, crawlability, canonical tags, and indexation against your actual route table. If the audit turns up architectural gaps, Forefront’s custom web development team builds the server-first rendering fixes directly into a custom-coded frontend, no template shortcuts. From there, ongoing maintenance keeps metadata, sitemaps, and status codes accurate as new routes ship. Businesses in South Florida can also see this applied locally through Forefront’s Miami web design and development and Palm Beach web design and development work. Start with the audit and see exactly which routes are holding your app back.

Sources

Want this applied to your own site?

Tell us what your site is not doing and we will tell you what we would change, no obligation.