Next.js SEO: Why Your SaaS Website Isn't Getting Indexed (And How to Fix It)
We've audited over 70 funded SaaS websites. More than half are built on Next.js. And a shocking number of them — companies that raised $5M, $20M, even $60M — have basic Next.js SEO errors that make Google treat their pages as blank. Here's every mistake we've found, and exactly how to fix each one.
The Next.js SEO Myth
There's a common belief among SaaS founders: "We're using Next.js, so our SEO is handled." It's not. Next.js is an excellent framework with first-class SEO capabilities — but those capabilities require correct configuration. A Next.js app with default settings, or with common misconfigurations, can be nearly invisible to Google.
The framework doesn't guarantee indexing. It gives you the tools. You have to use them correctly.
The 7 Most Common Next.js SEO Mistakes
BAILOUT_TO_CLIENT_SIDE_RENDERING
This is the most damaging Next.js SEO error. When a server component can't render fully on the server, Next.js falls back to pure client-side rendering. Googlebot sees an empty HTML shell with no content — just a loading spinner or blank div.
How it happens:
- Using
windowordocumentin a server component - Importing a client-only library (like chart.js, Framer Motion) directly in a server component
- Forgetting to add
'use client'to a component that uses React hooks - Using
useSearchParams()outside of a Suspense boundary
Check your browser console and server logs for BAILOUT_TO_CLIENT_SIDE_RENDERING. For each occurrence:
// Move browser-dependent code to a Client Component
'use client'
import { useState, useEffect } from 'react'
import SomeBrowserLibrary from 'some-browser-library'
export function ClientOnlySection() {
const [data, setData] = useState(null)
useEffect(() => {
// Browser code here — safe
}, [])
return <div>{data}</div>
}
Wrap useSearchParams() in a Suspense boundary:
import { Suspense } from 'react'
export default function Page() {
return (
<Suspense fallback={<div>Loading...</div>}>
<SearchParamsComponent />
</Suspense>
)
}
Missing or Wrong Canonical Tags
Canonical tags tell Google which URL is the "original" when the same content exists at multiple URLs. In Next.js, canonical issues show up as: empty canonical (<link rel="canonical" href="">), wrong canonical (pointing to a staging URL, localhost, or another page), or no canonical at all.
We've seen production SaaS sites where every page had canonical pointing to http://localhost:3000/ — meaning Google thought the entire site was duplicating localhost content. Every page was marked for de-indexing.
// app/page.tsx
export const metadata = {
alternates: {
canonical: 'https://yourdomain.com/',
},
}
// For dynamic routes: app/blog/[slug]/page.tsx
export async function generateMetadata({ params }) {
return {
alternates: {
canonical: `https://yourdomain.com/blog/${params.slug}`,
},
}
}
import Head from 'next/head'
export default function BlogPost({ post }) {
return (
<>
<Head>
<link rel="canonical" href={`https://yourdomain.com/blog/${post.slug}`} />
</Head>
{/* content */}
</>
)
}
Multiple H1 Tags
Google uses the H1 tag to understand what a page is about. Multiple H1s send conflicting signals. In Next.js, this happens when layout files (like app/layout.tsx) include an H1 that stacks with the page's own H1 — or when reusable Hero/Banner components each render an H1.
We found one funded SaaS startup with 5 H1 tags on their homepage — every major section had its own H1.
Audit first:
// Run in browser console
document.querySelectorAll('h1').length // should return 1
Then fix: layouts should never contain H1 tags. Only pages should. In reusable components, use H2 for section headings:
// ❌ Wrong — layout with H1
export default function Layout({ children }) {
return <div><h1>AutoSEOBot</h1>{children}</div>
}
// ✅ Right — layout without H1
export default function Layout({ children }) {
return <div><nav>...</nav>{children}</div>
}
Missing Metadata (Title, Description, OG Tags)
A default Next.js app has no <title>, no <meta description>, and no Open Graph tags unless you add them. Many teams add metadata to the homepage but forget product pages, blog posts, or documentation.
// app/layout.tsx — default metadata (fallback for all pages)
export const metadata = {
title: {
default: 'Your SaaS — Product Tagline',
template: '%s | Your SaaS',
},
description: 'Your default meta description here.',
openGraph: {
siteName: 'Your SaaS',
images: [{ url: 'https://yourdomain.com/og-image.png', width: 1200, height: 630 }],
},
}
// app/features/page.tsx — page-level override
export const metadata = {
title: 'Features',
description: 'Everything Your SaaS can do...',
alternates: { canonical: 'https://yourdomain.com/features' },
}
No Sitemap or a Broken Sitemap
Sitemaps tell Google what pages exist. In Next.js, a common mistake is either forgetting to create one, or having a sitemap that returns a 404 or 500 error. We've seen funded startups where /sitemap.xml returns a 404 — Google has no map of the site.
In Next.js 13+ App Router, add app/sitemap.ts:
import { MetadataRoute } from 'next'
export default function sitemap(): MetadataRoute.Sitemap {
return [
{ url: 'https://yourdomain.com', lastModified: new Date(), changeFrequency: 'weekly', priority: 1 },
{ url: 'https://yourdomain.com/features', lastModified: new Date(), changeFrequency: 'monthly', priority: 0.8 },
{ url: 'https://yourdomain.com/pricing', lastModified: new Date(), changeFrequency: 'monthly', priority: 0.8 },
// Add all public pages
]
}
Then submit https://yourdomain.com/sitemap.xml in Google Search Console.
Misconfigured robots.txt Blocking Googlebot
A single wrong line in robots.txt can make your entire site invisible to Google. The most dangerous pattern:
User-agent: *
Disallow: /
This blocks all crawlers from everything. We've seen live production SaaS sites with this exact configuration — usually copied from a .env.example or staging robots.txt that never got updated for production.
// app/robots.ts
import { MetadataRoute } from 'next'
export default function robots(): MetadataRoute.Robots {
return {
rules: {
userAgent: '*',
allow: '/',
disallow: ['/api/', '/admin/', '/dashboard/'],
},
sitemap: 'https://yourdomain.com/sitemap.xml',
}
}
No Structured Data (Schema Markup)
Schema markup helps Google understand what your page is about and can unlock rich results (FAQ dropdowns, star ratings, breadcrumbs). Most Next.js SaaS sites have zero schema markup.
// Add to any Next.js page
export default function PricingPage() {
const schema = {
"@context": "https://schema.org",
"@type": "SoftwareApplication",
"name": "Your SaaS",
"applicationCategory": "BusinessApplication",
"offers": {
"@type": "Offer",
"price": "49",
"priceCurrency": "USD"
}
}
return (
<>
<script
type="application/ld+json"
dangerouslySetInnerHTML={{ __html: JSON.stringify(schema) }}
/>
{/* page content */}
</>
)
}
The Next.js SEO Audit Checklist
Before deploying any Next.js site, run through this checklist:
- ✅ No
BAILOUT_TO_CLIENT_SIDE_RENDERINGwarnings in server logs - ✅ Every public page has a unique
<title>(50–60 characters) - ✅ Every public page has a
<meta description>(120–160 characters) - ✅ Every public page has a canonical pointing to itself
- ✅ Exactly one
<h1>per page - ✅
/sitemap.xmlreturns 200 and lists all public pages - ✅
/robots.txtallows Googlebot (check:Disallow: /is not present in production) - ✅ Open Graph tags present for social sharing
- ✅ Schema markup on homepage (Organization/WebSite) and key landing pages
- ✅ No
noindexmeta tags on production pages you want indexed - ✅ Verify in Google Search Console: "URL Inspection" shows "URL is on Google"
curl -s https://yourdomain.com/your-page | grep -i "<title\|description\|canonical\|h1" to quickly verify server-side rendered HTML. If your title, description, canonical, and H1 don't appear in the curl output — Google can't see them either.
How to Verify Google Can Actually Index Your Next.js Pages
- Google Search Console → URL Inspection — Paste any URL and check "Coverage" status. "URL is on Google" = indexed. "Crawled — currently not indexed" or "Discovered — currently not indexed" = problem.
- curl test — Run
curl -s https://yourdomain.com | grep -i "title\|description". If your page title doesn't appear in the raw HTML response, Googlebot can't see it. - Rich Results Test — Google's tool at search.google.com/test/rich-results shows exactly what structured data Google finds.
- Mobile Usability — Google uses mobile-first indexing. Check search.google.com/search-console/mobile-usability for issues.
Next.js App Router vs Pages Router: SEO Differences
If you're still on the Pages Router, you can still implement good SEO — but App Router makes it significantly easier:
- App Router:
generateMetadata()— dynamic metadata at the route level. Cleaner, more composable, auto-deduplicates tags. - Pages Router:
next/head— works but requires manual deduplication. If you accidentally include two<title>tags via nested<Head>components, behavior is unpredictable. - App Router: Server Components by default — better for SEO because content renders on the server.
- Pages Router: Requires
getServerSidePropsorgetStaticPropsto render content server-side.
If you're building a new SaaS site, use App Router. If you're on Pages Router and SEO is suffering, a migration to App Router is worth planning for.
When to Bring In Professional Help
If you've fixed the technical issues above and you're still not ranking, the problem shifts from technical to strategic: the right keywords, content depth, backlink authority, and competitive positioning. That's where an SEO audit from a team that understands both SaaS and Next.js pays off.
The signals that suggest you need professional help:
- You've had a Next.js site for 6+ months with no organic traffic growth
- Google Search Console shows pages "Crawled — currently not indexed" even after technical fixes
- Your competitors are ranking for your exact keywords despite apparently similar content
- You're not sure which keywords to target or whether your content strategy makes sense
See also: Technical SEO Checklist for SaaS · Why Your SaaS Website Isn't Ranking · Fix Noindex & Canonical Issues
Get a Free Next.js SEO Audit
We'll audit your Next.js site for every issue on this list — plus content gaps, backlink opportunities, and GEO readiness. Free, delivered in 48 hours.
Get Your Free Audit →Frequently Asked Questions
export const metadata = { alternates: { canonical: 'https://yourdomain.com/your-page' } }. For the Pages Router, use next/head: <link rel='canonical' href='...' />. Make sure every page has a unique canonical pointing to itself — duplicate or missing canonicals cause index dilution.document.querySelectorAll('h1').length in browser console. Fix by ensuring only one H1 per page — usually the main headline. Use H2–H6 for subheadings.