SEO for Web Apps — React & Next.js | Symaxx Resources
Master SEO for modern web applications built with React, Next.js, Vue, and other JavaScript frameworks. Covers routing, rendering, metadata, and crawlability.
Web applications built with React, Next.js, Vue, or Angular present unique SEO challenges that traditional websites do not face. Client-side routing, JavaScript-dependent rendering, dynamic metadata, and hybrid public/private architectures require deliberate SEO engineering — not as an afterthought, but as a core architectural decision.
- Single-Page Applications (SPAs) using client-side routing are inherently difficult for search engines to crawl and index.
- Next.js App Router solves most SPA SEO problems with server components, static generation, and built-in metadata APIs.
- Separate public (indexable) and private (dashboard) routes architecturally — they have fundamentally different SEO requirements.
- Always render metadata server-side — client-side
<title>and<meta>tags may not be seen by crawlers. - Use the metadata API in Next.js (
metadataexport orgenerateMetadata()) instead of<Head>components. - Test crawlability by viewing your page source — if content is not in the raw HTML, it may not be indexed.
If you want the full breakdown, continue below.
The SPA SEO Problem
Traditional SPAs (React with React Router, Vue with Vue Router) work like this:
- Server sends a minimal HTML shell (
<div id="root"></div>) - JavaScript loads and renders the entire page in the browser
- Client-side routing changes URLs without server requests
Why this breaks SEO:
- Googlebot sees an empty HTML shell on initial crawl
- JavaScript rendering happens in a separate, delayed queue
- Client-side routing may not produce proper HTTP responses (no 404 for missing pages)
- Metadata is set via JavaScript, which crawlers may not execute
Framework-Specific SEO Solutions
Next.js App Router (Recommended)
Next.js with the App Router is the most SEO-friendly React framework:
Server Components (default):
// This renders on the server — full HTML sent to crawler
export default async function ServicePage() {
return (
<main>
<h1>Web Design Services</h1>
<p>Professional web design for South African businesses.</p>
</main>
)
}
Metadata API:
// Static metadata
export const metadata = {
title: 'Web Design Services — Symaxx Digital',
description: 'Professional web design for SA businesses.',
openGraph: {
title: 'Web Design Services',
description: 'Professional web design for SA businesses.',
},
}
// Dynamic metadata
export async function generateMetadata({ params }) {
const service = await getService(params.slug)
return {
title: `${service.name} — Symaxx Digital`,
description: service.description,
}
}
Advantages:
- Server rendering by default (no JavaScript required for indexing)
- Built-in metadata API (no third-party components)
- Static generation for content pages
- Streaming SSR for dynamic pages
- Built-in image optimisation (
next/image) - Automatic code splitting
React SPA (Create React App / Vite)
If you must use a pure React SPA for public pages:
- Add pre-rendering: Use
react-snaporprerender.ioto generate static HTML - Server-side metadata: Use a pre-rendering service that executes JavaScript for crawlers
- Consider migration: For SEO-critical pages, strongly consider migrating to Next.js
Vue / Nuxt
Nuxt is Vue's equivalent of Next.js:
- Use
useHead()for metadata - Default SSR mode for SEO
useSeoMeta()for structured meta tags
Architectural Best Practices
Separate Public and Private Routes
/ → Public (SSG, indexable)
/services/* → Public (SSG, indexable)
/blog/* → Public (SSG, indexable)
/resources/* → Public (SSG, indexable)
/dashboard/* → Private (CSR, noindex)
/admin/* → Private (CSR, noindex)
/api/* → Private (not pages)
Implementation in Next.js:
// app/(public)/layout.tsx — public group (SEO-optimised)
export default function PublicLayout({ children }) {
return (
<>
<Header />
<main>{children}</main>
<Footer />
</>
)
}
// app/(dashboard)/layout.tsx — private group (no SEO)
export const metadata = { robots: 'noindex, nofollow' }
export default function DashboardLayout({ children }) {
return <DashboardShell>{children}</DashboardShell>
}
Dynamic Routes and SEO
For dynamic routes (e.g., /blog/[slug]), generate static params at build time:
export async function generateStaticParams() {
const posts = await getAllPosts()
return posts.map(post => ({ slug: post.slug }))
}
This ensures all dynamic pages are pre-rendered as static HTML.
Handling 404s Properly
SPAs often show a client-side "not found" component without returning a proper 404 HTTP status code. Google sees this as a "soft 404" and may continue crawling the non-existent URL.
// Next.js App Router — proper 404
import { notFound } from 'next/navigation'
export default async function Page({ params }) {
const data = await getData(params.slug)
if (!data) notFound() // Returns proper 404 status code
return <PageContent data={data} />
}
Sitemap Generation
Generate XML sitemaps dynamically from your content:
// app/sitemap.ts
export default async function sitemap() {
const posts = await getAllPosts()
return [
{ url: 'https://symaxx.co.za', lastModified: new Date() },
...posts.map(post => ({
url: `https://symaxx.co.za/blog/${post.slug}`,
lastModified: post.updatedAt,
})),
]
}
Common Web App SEO Mistakes
Using CSR for public pages. Any page that should be indexable must be server-rendered.
Setting metadata on the client. If <title> is set by JavaScript after page load, crawlers may not see it. Always use server-side metadata APIs.
No canonical tags on dynamic routes. If the same content is accessible via multiple URLs (parameters, filters, pagination), set canonical tags.
Heavy JavaScript bundles on public pages. Large JS bundles slow page load, hurt Core Web Vitals, and may cause rendering timeouts for Googlebot.
Not generating static params. Dynamic routes without generateStaticParams are rendered on-demand, which is slower and may not be crawled as efficiently.
Forgetting robots.txt for app routes. Dashboard and API routes should be disallowed in robots.txt.
Performance Considerations
Web apps tend to ship more JavaScript than traditional sites:
- Code split aggressively — only load JS needed for the current page
- Use React Server Components — zero JavaScript shipped to the client
- Lazy load heavy components — charts, maps, editors loaded on demand
- Optimise images with
next/image(automatic WebP, lazy loading, sizing) - Monitor Core Web Vitals — web apps are at higher risk of CWV failures
Key Takeaways
- Next.js App Router is the gold standard for SEO in React applications.
- Server components render by default — ensuring content is always crawlable.
- Separate public and private routes architecturally (different layouts, different SEO treatment).
- Always use server-side metadata APIs, never client-side
<Head>components for SEO tags. - Generate static params for dynamic routes to pre-render all public pages.
- Return proper 404 status codes for missing pages, not soft 404s.
- Minimise JavaScript on public pages for Core Web Vitals compliance.
Quick Web App SEO Checklist
- All public pages use SSG or SSR (not CSR)
- Metadata set via
metadataexport orgenerateMetadata()(server-side) - Dynamic routes use
generateStaticParams()for static pre-rendering - Private routes marked
noindex, nofollow - Proper 404 responses for missing pages (using
notFound()) - XML sitemap auto-generated from content
robots.txtdisallows dashboard and API routes- Canonical tags set for pages accessible via multiple URLs
- Core Web Vitals passing on all public pages
- JavaScript bundle size monitored and code-split
Related SEO Documentation
Was this helpful?