
Next.js SEO Complete Guide
1. Global SEO Setup (Root Layout)
Start by defining global metadata in:
app/layout.js
export const metadata = {
metadataBase: new URL("https://yourdomain.com"),
title: {
default: "Your Brand",
template: "%s | Your Brand",
},
description: "Your brand default description.",
openGraph: {
siteName: "Your Brand",
type: "website",
locale: "en_US",
},
twitter: {
card: "summary_large_image",
},
robots: {
index: true,
follow: true,
},
};Now let’s clearly explain what each part does.
metadataBase
metadataBase: new URL("https://yourdomain.com"),Defines your main domain.
Used to generate absolute URLs for:
Canonical links
Open Graph URLs
Social previews
Without this, relative URLs may not resolve correctly.
title.default
default: "Your Project Name"Used when a page does not define its own title.
Acts as a fallback title.
title.template
template: "%s | Your Project Name"Automatically formats page titles.
%sis replaced by the page title.
Example:
If a page sets:
title: "About"The final title becomes:
About | Your Project Name
This keeps branding consistent across the site.
description
description: "Your brand default description."Default meta description.
Used when a page does not define its own description.
Appears in Google search results.
Every important page should override this.
openGraph
openGraph: {
siteName: "Your Brand",
type: "website",
locale: "en_US",
}Controls how your website appears when shared on:
Facebook
LinkedIn
WhatsApp
Slack
Fields:
siteName→ Displayed site name in previewstype→ Usually "website" or "article"locale→ Language version of your site
Individual pages can override this.
twitter: {
card: "summary_large_image",
}Controls Twitter/X preview style.
summary_large_image means:
Big preview image
Better engagement
Other options:
summaryappplayer
robots
robots: {
index: true,
follow: true,
}Tells search engines:
index: true→ This page can appear in search results.follow: true→ Follow links on this page.
If you set:
index: falseGoogle will not index the page.
Used for:
Admin pages
Private dashboards
Internal tools
2. Static Page SEO (About, Contact, Landing Pages)
Start by defining global metadata in:
app/about/page.jsx
export const metadata = {
title: "About Us",
description: "Learn more about our company and mission.",
alternates: {
canonical: "/about",
},
};
export default function AboutPage() {
return <div>About Page</div>;
}Use this for:
About
Contact
Privacy policy
Static landing pages
No need for generateMetadata() here.
3. Dynamic Page SEO (Blog / Product Pages)
Dynamic routes require dynamic metadata.
Example:
app/blog/[slug]/page.jsx
Fetch Data
async function getPost(slug) {
const res = await fetch(`https://api.example.com/posts/${slug}`, {
next: { revalidate: 60 },
});
if (!res.ok) return null;
return res.json();
}Add generateMetadata()
export async function generateMetadata({ params }) {
const { slug } = await params;
const post = await getPost(slug);
if (!post) {
return {
title: "Post Not Found",
};
}
return {
title: post.seoTitle || post.title,
description: post.seoDescription || post.excerpt,
alternates: {
canonical: `/blog/${slug}`,
},
openGraph: {
title: post.title,
description: post.excerpt,
images: [
{
url: post.featuredImage,
width: 1200,
height: 630,
},
],
type: "article",
},
};
}Important:
Always
await paramsAlways set canonical
Always define an Open Graph image
Use
revalidatefor SEO-friendly caching
4. Open Graph Setup (Social Sharing)
Open Graph controls previews on:
Facebook
LinkedIn
WhatsApp
Slack
Minimum recommended setup:
openGraph: {
title: "Page Title",
description: "Page description",
url: "https://yourdomain.com/page",
images: [
{
url: "https://yourdomain.com/og-image.jpg",
width: 1200,
height: 630,
},
],
type: "website",
}Image size recommendation:
1200 × 630 px
5. Dynamic Open Graph Image (Advanced but Powerful)
Instead of uploading images manually for each blog post, generate them dynamically.
Create:
app/blog/[slug]/opengraph-image.jsx
import { ImageResponse } from "next/og";
export const size = {
width: 1200,
height: 630,
};
export default async function Image({ params }) {
const { slug } = await params;
return new ImageResponse(
(
<div
style={{
background: "#111",
color: "white",
width: "100%",
height: "100%",
display: "flex",
justifyContent: "center",
alignItems: "center",
fontSize: 60,
}}
>
{slug}
</div>
),
size
);
}Now every blog page automatically gets a share image.
6. Add Structured Data (JSON-LD)
Add this inside your dynamic page component:
<script
type="application/ld+json"
dangerouslySetInnerHTML={{
__html: JSON.stringify({
"@context": "https://schema.org",
"@type": "Article",
headline: post.title,
description: post.excerpt,
image: post.featuredImage,
author: {
"@type": "Person",
name: "Your Name",
},
}),
}}
/>This improves:
Rich results
Google understanding
Content classification
7. Canonical URL Setup
Prevents duplicate content issues.
Example:
alternates: {
canonical: "/blog/nextjs-seo-guide",
}Very important for:
Pagination
Filtered product pages
Query parameters
8. Create Sitemap
Create:
app/sitemap.js
export default async function sitemap() {
const posts = await fetch("https://api.example.com/posts").then(res => res.json());
return [
{
url: "https://yourdomain.com",
lastModified: new Date(),
},
...posts.map(post => ({
url: `https://yourdomain.com/blog/${post.slug}`,
lastModified: new Date(post.updatedAt),
})),
];
}Your sitemap will be available at:
/sitemap.xml
9. Add robots.txt
Create:
app/robots.js
export default function robots() {
return {
rules: {
userAgent: "*",
allow: "/",
},
sitemap: "https://yourdomain.com/sitemap.xml",
};
}10. Final Production SEO Checklist
Before publishing:
Unique title for every page
Unique description for every page
Canonical URL set
Open Graph image defined
Sitemap working
robots.txt working
Dynamic pages using
generateMetadata()No duplicate content issues
Good Core Web Vitals
Final Folder Structure
app/
layout.js
sitemap.js
robots.js
about/page.jsx
blog/
[slug]/
page.jsx
opengraph-image.jsx