Next.js SEO: The Founder Setup Guide
Ship Next.js SEO right. Metadata, sitemaps, rendering, canonicals, and schema in one guide. Step-by-step for founders who need organic visibility fast.
The Problem With Next.js SEO Defaults
You shipped. Your Next.js app works. Users love it. But Google doesn't know it exists.
Next.js is fast and modern, but its SEO defaults are incomplete. Metadata doesn't propagate correctly. Your sitemap might not exist. Dynamic pages render server-side, but search engines see a blank shell. Canonicals aren't set. Schema markup is missing. You've built something good, and the search engine can't crawl it properly.
This isn't a Next.js problem—it's a founder problem. You optimized for speed and features, not for discoverability. By the time you realize SEO matters, you've already shipped without the foundational signals Google rewards.
This guide fixes that. In under an hour, you'll have Next.js SEO configured correctly: proper metadata, a working sitemap, server-side rendering locked down, canonicals enforced, and structured data in place. You'll have the same SEO foundation that indie hackers and bootstrappers use when they need organic visibility fast—without paying an agency.
Prerequisites: What You Need Before Starting
Before you touch code, confirm you have these in place:
- A Next.js 13+ project (App Router preferred, but Pages Router works too)
- A production domain (not localhost, not a staging URL)
- Access to your DNS settings (you'll need this for verification)
- Google Search Console access (free; sign up at Google Search Console)
- A code editor and git (to commit your changes)
- 10 minutes per section (this is not a 5-minute setup)
If you're using the Pages Router, the principles here still apply—the implementation details just differ slightly. If you're on Vercel (the easiest path for Next.js), you'll have some of these configured automatically. We'll note where.
One more thing: this guide assumes you have a working Next.js build. If you haven't deployed yet, deploy first. SEO setup only matters once Google can actually reach your site.
Step 1: Set Up Metadata and Open Graph Tags
Google reads your <head> tags to understand what your page is about. Next.js gives you multiple ways to set these, and founders usually get it wrong by using the old way or by setting metadata inconsistently across pages.
The Right Way: Use Metadata Objects
In Next.js 13+ with the App Router, metadata goes in your layout.tsx or individual page.tsx files. Don't use the old <Head> component from next/head—it's deprecated for this.
Open your root layout.tsx:
import type { Metadata } from 'next';
export const metadata: Metadata = {
title: 'Your Product Name | What It Does',
description: 'One sentence describing your product. Keep it under 160 characters.',
keywords: 'keyword1, keyword2, keyword3',
openGraph: {
title: 'Your Product Name | What It Does',
description: 'One sentence describing your product.',
url: 'https://yourdomain.com',
siteName: 'Your Product Name',
images: [
{
url: 'https://yourdomain.com/og-image.png',
width: 1200,
height: 630,
alt: 'Your Product Name',
},
],
type: 'website',
},
twitter: {
card: 'summary_large_image',
title: 'Your Product Name | What It Does',
description: 'One sentence describing your product.',
images: ['https://yourdomain.com/og-image.png'],
},
canonical: 'https://yourdomain.com',
};
This is your global fallback. Every page inherits this unless you override it.
For individual pages, create a metadata export in each page.tsx:
import type { Metadata } from 'next';
export const metadata: Metadata = {
title: 'Specific Page Title | Your Product',
description: 'Specific description for this page.',
};
export default function Page() {
return <div>Your page content</div>;
}
What Founders Get Wrong
They forget to set openGraph and twitter tags. When someone shares your link on Twitter or Slack, it shows up as a blank card with no image. That's a conversion killer and a signal to Google that your site isn't trustworthy.
They also set the same title and description on every page. Google sees duplicate metadata and assumes your site is thin or auto-generated. Make each page's metadata specific to that page's content.
Pro Tip: Use Dynamic Metadata
If you have product pages or blog posts, generate metadata dynamically:
export async function generateMetadata(
{ params }: { params: { slug: string } }
): Promise<Metadata> {
const post = await getPost(params.slug);
return {
title: post.title,
description: post.excerpt,
openGraph: {
title: post.title,
description: post.excerpt,
url: `https://yourdomain.com/blog/${params.slug}`,
images: [{ url: post.imageUrl, width: 1200, height: 630 }],
},
};
}
This scales metadata across hundreds of pages without manual work.
Step 2: Generate and Submit Your Sitemap
Google crawls your site by following links, but a sitemap tells Google exactly which pages exist and how often they change. Without a sitemap, new pages might take weeks to get indexed.
Create a Sitemap for Next.js
In your app directory, create a file called 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/pricing',
lastModified: new Date(),
changeFrequency: 'monthly',
priority: 0.8,
},
{
url: 'https://yourdomain.com/about',
lastModified: new Date(),
changeFrequency: 'monthly',
priority: 0.5,
},
];
}
Next.js will automatically serve this at /sitemap.xml. But most founders have dynamic content—blog posts, product listings, user profiles. You need to generate the sitemap dynamically.
For a blog, fetch all posts and add them:
export default async function sitemap(): Promise<MetadataRoute.Sitemap> {
const posts = await getAllPosts();
const postUrls = posts.map((post) => ({
url: `https://yourdomain.com/blog/${post.slug}`,
lastModified: post.updatedAt,
changeFrequency: 'weekly' as const,
priority: 0.7,
}));
return [
{
url: 'https://yourdomain.com',
lastModified: new Date(),
changeFrequency: 'weekly' as const,
priority: 1,
},
...postUrls,
];
}
For a comprehensive guide on generating sitemaps across different tech stacks, check out How to Generate a Sitemap.xml for Your Site (Every Stack Covered).
Submit Your Sitemap to Google
Once your sitemap is live at https://yourdomain.com/sitemap.xml, you need to tell Google it exists. Go to Google Search Console, select your domain property, and submit the sitemap URL in the Sitemaps section.
You should also add it to your robots.txt file (we'll cover that in Step 4).
For a step-by-step walkthrough on setting up Google Search Console and submitting your sitemap, see How to Set Up Google Search Console in 10 Minutes.
Step 3: Configure Rendering and Ensure Crawlability
This is where most Next.js sites fail SEO. Your pages might render beautifully in the browser, but Google's crawler sees something different.
Use Server-Side Rendering (SSR) or Static Generation
Google's crawler can execute JavaScript, but it's slow and unreliable. If you're using client-side rendering (CSR) with useEffect to fetch data, Google sees a blank page while it waits for your API calls to complete.
The fix: use Server-Side Rendering or Static Generation.
For pages that don't change often (landing pages, docs, about pages): Use generateStaticParams and static generation:
export async function generateStaticParams() {
return [{ slug: 'about' }, { slug: 'pricing' }];
}
export default function Page({ params }: { params: { slug: string } }) {
return <div>Page content</div>;
}
Next.js builds these pages at build time and serves them as static HTML. Google sees the full page instantly.
For pages that change frequently (user profiles, real-time data): Use Server-Side Rendering:
export default async function Page({ params }: { params: { id: string } }) {
const data = await fetch(`/api/user/${params.id}`, {
next: { revalidate: 60 }, // Revalidate every 60 seconds
});
return <div>{/* Render data */}</div>;
}
The key: render on the server, not the client. If Google sees your content server-side, it can index it.
Check Rendering on Google Search Console
Go to Google Search Console, select your property, and use the URL Inspection tool. Click "Test live URL" and see what Google actually sees when it crawls your page.
If the rendered HTML is missing content, you're using CSR incorrectly. Switch to SSR or static generation.
For more on ensuring your site is crawlable and indexed properly, see Robots, Sitemaps, and Canonicals: The Three Files Founders Always Get Wrong.
Step 4: Set Up Robots.txt and Enforce Canonicals
Your robots.txt file tells Google which pages to crawl and which to skip. Your canonical tags tell Google which version of a page is the "official" one.
Create robots.txt
In your public directory, create a robots.txt file:
User-agent: *
Allow: /
Disallow: /admin
Disallow: /api
Disallow: /*.json$
Sitemap: https://yourdomain.com/sitemap.xml
This tells Google:
- Crawl everything (
Allow: /) - Don't crawl admin pages or API routes
- Here's where to find the sitemap
If you have pages you don't want indexed, add them to Disallow. But be careful—disallowing pages doesn't prevent them from being indexed if they're linked from elsewhere. Use noindex in the metadata for that.
Enforce Canonical URLs
If your site is accessible at both yourdomain.com and www.yourdomain.com, or at both HTTP and HTTPS, Google sees these as separate sites. You need to pick one and enforce it.
Set the canonical in your metadata:
export const metadata: Metadata = {
title: 'Your Title',
description: 'Your description',
canonical: 'https://yourdomain.com/your-page',
};
Also redirect all non-canonical URLs to the canonical one. If you're on Vercel, add a vercel.json file:
{
"redirects": [
{
"source": "/:path*",
"destination": "https://yourdomain.com/:path*",
"permanent": true
}
]
}
For a detailed guide on choosing and enforcing your canonical domain, see WWW vs. Non-WWW: Choosing and Enforcing Your Canonical Domain.
Step 5: Add Structured Data (Schema Markup)
Structured data tells Google what your page is about in a machine-readable format. It's how you get rich results—star ratings, product prices, breadcrumbs, FAQ sections.
Google prefers JSON-LD format. In Next.js, add it to your layout.tsx or individual pages:
export default function Page() {
const schemaData = {
'@context': 'https://schema.org',
'@type': 'Product',
name: 'Your Product Name',
description: 'Your product description',
price: '99',
priceCurrency: 'USD',
image: 'https://yourdomain.com/product-image.jpg',
brand: {
'@type': 'Brand',
name: 'Your Brand',
},
};
return (
<>
<script
type="application/ld+json"
dangerouslySetInnerHTML={{ __html: JSON.stringify(schemaData) }}
/>
<div>Your page content</div>
</>
);
}
For a blog post, use the BlogPosting schema:
const schemaData = {
'@context': 'https://schema.org',
'@type': 'BlogPosting',
headline: 'Your Post Title',
description: 'Your post excerpt',
image: 'https://yourdomain.com/post-image.jpg',
datePublished: '2024-01-15',
dateModified: '2024-01-20',
author: {
'@type': 'Person',
name: 'Your Name',
},
};
Test your schema with Google's Rich Results Test. If there are errors, fix them before deploying.
For a comprehensive guide on implementing schema markup correctly, see Setting Up Schema Markup with Google's Rich Results Test and Introduction to structured data from Google's web.dev.
Also check Structured Data General Policies to ensure your implementation follows Google's guidelines and won't trigger penalties.
Step 6: Verify Your Domain in Google Search Console
You can't see Google's crawl data or submit sitemaps without verifying your domain. There are multiple verification methods—pick the one that works for your setup.
DNS Verification (Recommended)
Add a DNS record to your domain registrar. This is permanent and survives site migrations.
Go to Google Search Console, click "Verify" and select DNS. Google will give you a TXT record. Add it to your DNS provider (GoDaddy, Namecheap, etc.).
Wait a few minutes for DNS to propagate, then Google Search Console will confirm verification.
HTML File or Meta Tag
If you can't access DNS, upload an HTML file to your root directory or add a meta tag to your layout.tsx.
For detailed instructions on all verification methods, see Verifying Your Domain in Google Search Console: Every Method Explained.
Step 7: Enable HTTPS and Fix Mixed Content
Google ranks HTTPS sites higher than HTTP. If your site is still on HTTP, switch now.
If you're on Vercel, HTTPS is automatic. If you're self-hosting, get an SSL certificate from Let's Encrypt (free) or your hosting provider.
Once HTTPS is enabled, make sure all resources (images, scripts, stylesheets) load over HTTPS, not HTTP. If they don't, you'll see "mixed content" warnings and Google will penalize you.
In your Next.js code, use relative URLs instead of absolute HTTP URLs:
// Bad
<img src="http://yourdomain.com/image.jpg" />
// Good
<img src="/image.jpg" />
// Also good
<img src="https://yourdomain.com/image.jpg" />
For a complete guide on SSL setup and mixed content fixes, see SSL Certificates and SEO: Setting Up HTTPS the Right Way.
Step 8: Optimize Core Web Vitals
Google's Core Web Vitals are ranking factors. They measure how fast your site loads and how responsive it is.
The three metrics:
- Largest Contentful Paint (LCP): How fast the main content loads (target: under 2.5 seconds)
- First Input Delay (FID): How responsive the site is to user input (target: under 100ms)
- Cumulative Layout Shift (CLS): How much the page jumps around while loading (target: under 0.1)
Next.js is already fast, but you can make it faster.
Image Optimization
Use Next.js's Image component instead of <img>:
import Image from 'next/image';
export default function Page() {
return (
<Image
src="/image.jpg"
alt="Description"
width={1200}
height={630}
priority // Load this image first
/>
);
}
Next.js automatically optimizes images, serves them in modern formats (WebP), and resizes them for different devices.
Code Splitting
Use dynamic imports to load heavy components only when needed:
import dynamic from 'next/dynamic';
const HeavyComponent = dynamic(() => import('./HeavyComponent'), {
loading: () => <p>Loading...</p>,
});
export default function Page() {
return <HeavyComponent />;
}
Check Your Scores
Run Google PageSpeed Insights on your live site. It will show you Core Web Vitals and suggest fixes.
For a guide on setting up and interpreting PageSpeed Insights, see Setting Up PageSpeed Insights and Reading Your First Report.
Step 9: Link Google Analytics and Google Search Console
Google Search Console shows you search traffic and keywords. Google Analytics shows you user behavior. Together, they tell you what's working.
Set Up Google Analytics 4
Create a GA4 property and add the tracking code to your Next.js site. In your layout.tsx:
import { GoogleAnalytics } from '@next/third-parties/google';
export default function RootLayout() {
return (
<html>
<body>
{/* Your content */}
<GoogleAnalytics gaId="G-XXXXXXXXXX" />
</body>
</html>
);
}
For a complete GA4 setup guide, see Setting Up Google Analytics 4 for SEO Tracking from Day One.
Link GA4 to Google Search Console
In Google Search Console, go to Settings > Linked accounts and link your GA4 property. This adds search data to GA4, so you can see which keywords bring traffic.
For step-by-step instructions, see Linking GA4 with Google Search Console: The 2-Minute Setup.
Once linked, check The 5 GA4 Reports Every Busy Founder Should Bookmark to focus on metrics that actually matter for SEO.
Step 10: Set Up IndexNow for Faster Indexing
When you publish a new page, Google might take days to crawl it. IndexNow tells Bing and Yandex instantly that a page exists.
While Bing's market share is smaller than Google's, IndexNow is free and takes 10 minutes to set up. It's worth doing.
Enable IndexNow in Next.js
Create an indexnow.txt file in your public directory with a unique key:
00000000-0000-0000-0000-000000000000
Then create an indexnow.xml route in your app:
// app/indexnow.xml/route.ts
export async function GET() {
return new Response(
`<?xml version="1.0" encoding="UTF-8"?>
<urlList>
<url>https://yourdomain.com/page1</url>
<url>https://yourdomain.com/page2</url>
</urlList>`,
{
headers: { 'Content-Type': 'application/xml' },
}
);
}
Then submit your sitemap to IndexNow at IndexNow.
For a complete setup guide, see IndexNow Setup: Pinging Bing and Yandex for Faster Crawls.
Bonus: Use the next-seo Package for Consistency
If you're managing dozens of pages and want to ensure metadata is set correctly everywhere, the garmeeh/next-seo package simplifies things. It provides reusable components for common metadata patterns.
Install it:
npm install next-seo
Use it in your pages:
import { NextSeo } from 'next-seo';
export default function Page() {
return (
<>
<NextSeo
title="Your Page Title"
description="Your page description"
canonical="https://yourdomain.com/page"
openGraph={{
url: 'https://yourdomain.com/page',
title: 'Your Page Title',
description: 'Your page description',
images: [
{
url: 'https://yourdomain.com/og-image.jpg',
width: 1200,
height: 630,
alt: 'Your Page Title',
},
],
site_name: 'Your Site Name',
}}
/>
<div>Your page content</div>
</>
);
}
This keeps your metadata consistent and reduces boilerplate.
For more on Next.js SEO best practices, check out SEO | Next.js from the official Next.js documentation and The Complete Next.js SEO Guide for Building Crawlable Apps from Strapi.
Also see Next.js SEO: Complete Implementation Guide for 2026 for production-ready implementation patterns.
Verification Checklist: Before You Ship
Before you consider this done, run through this checklist:
- Metadata is set on your root layout and all major pages
- Open Graph and Twitter tags are populated with images
- Sitemap exists at
/sitemap.xmland includes all pages - Robots.txt exists and points to your sitemap
- Canonical URLs are set and enforced
- Pages render server-side (not blank in Google Search Console's URL Inspection tool)
- HTTPS is enabled and all resources load over HTTPS
- Domain is verified in Google Search Console
- Sitemap is submitted to Google Search Console
- Google Analytics 4 is installed and firing
- GA4 is linked to Google Search Console
- Schema markup is added to key pages and passes Google's Rich Results Test
- Core Web Vitals score is green (or at least not red) in PageSpeed Insights
If all of these are checked, your Next.js site has a proper SEO foundation.
What Comes Next
Now that your technical SEO is in place, focus on content. Write pages that answer real questions your users ask. Link them together. Build backlinks by sharing your work.
But here's the thing: without the technical foundation you just built, none of that matters. Google won't crawl your pages. It won't understand what they're about. It won't rank them.
You've now fixed that. Your Next.js site is crawlable, indexable, and set up to rank. The rest is execution.
If you need a quick SEO audit to find other issues, or if you want AI-generated blog content to fill out your site's topical authority, check out Seoable. We deliver a domain audit, brand positioning, keyword roadmap, and 100 AI-generated blog posts in under 60 seconds for a one-time $99 fee. It's built for founders who ship but lack organic visibility.
But for now, deploy these changes. Verify your domain. Submit your sitemap. Then watch Google start crawling your site.
Key Takeaways
- Metadata matters. Set title, description, and Open Graph tags on every page. Make them specific, not duplicated.
- Render server-side. Next.js is fast at SSR. Use it. Don't rely on client-side rendering for SEO.
- Build a sitemap. Dynamic sitemaps scale across hundreds of pages without manual work.
- Enforce canonicals. Pick one domain format (www or non-www, HTTP or HTTPS) and redirect everything to it.
- Add schema markup. JSON-LD structured data helps Google understand your content and can earn you rich results.
- Verify and monitor. Google Search Console is free and essential. Set it up. Check it weekly.
- Core Web Vitals matter. Fast sites rank higher. Next.js Image optimization and code splitting are your friends.
- Link your tools. GA4 + Google Search Console together show you what keywords drive traffic and which pages convert.
You've now done what most founders skip: built SEO into your Next.js app from day one. You didn't hire an agency. You didn't spend thousands. You spent an hour and got the same foundation that indie hackers and bootstrappers use to get organic visibility fast.
Now go ship.
Get the next one on Sunday.
One short email a week. What is working in SEO right now. Unsubscribe in one click.
Subscribe on Substack →