Module 25 — Metadata and SEO in Next.js
SEO is an important part of a production Next.js application.
A page can have excellent content and still perform poorly in search if search engines cannot clearly understand:
- What the page is about
- What title should appear in search results
- What description represents the page
- Which URL is canonical
- Whether the page should be indexed
- Which image should appear when the page is shared
Next.js provides a built-in Metadata API for managing these values.
The architecture is:
1Page 2 ↓ 3Metadata 4 ↓ 5Search Engine 6 ↓ 7Search Result
For dynamic pages:
1URL 2 ↓ 3Route Parameters 4 ↓ 5Fetch Data 6 ↓ 7generateMetadata() 8 ↓ 9Dynamic SEO
What Is Metadata?
Metadata is information about a web page that is primarily consumed by browsers, search engines, and social platforms.
For example:
1Title 2Description 3Canonical URL 4Robots 5Open Graph 6Twitter/X metadata
A simplified HTML result might contain:
1<title>Next.js Masterclass</title> 2 3<meta 4 name="description" 5 content="Learn Next.js from fundamentals to production architecture." 6/>
You normally do not need to manually write these tags in your JSX.
Next.js can generate them through its Metadata API.
Why Metadata Matters
Consider two pages.
Poor metadata
1Title: 2Home 3 4Description: 5Welcome to our website.
Useful metadata
1Title: 2Next.js Masterclass — Learn Next.js 3 4Description: 5Learn Next.js with Server Components, APIs, 6data fetching, authentication, and production architecture.
The second version gives search engines and users much more context.
A good SEO architecture is:
1Content 2 + 3Metadata 4 + 5Canonical URL 6 + 7Structured architecture 8 ↓ 9Search-friendly page
Metadata alone does not guarantee rankings, but it helps search engines understand and represent your pages correctly.
Metadata API
Next.js provides metadata through the Metadata type.
Example:
1import type { Metadata } from "next"; 2 3export const metadata: Metadata = { 4 title: "Next.js Masterclass", 5 description: 6 "Learn Next.js from fundamentals to production architecture.", 7};
This can be placed in a page or layout.
Static Metadata
Static metadata is appropriate when the metadata does not change based on the URL or fetched data.
Example:
1import type { Metadata } from "next"; 2 3export const metadata: Metadata = { 4 title: "Next.js Course", 5 description: 6 "Learn Next.js, Server Components, APIs, authentication, and production architecture.", 7}; 8 9export default function CoursePage() { 10 return ( 11 <main> 12 <h1>Next.js Course</h1> 13 </main> 14 ); 15}
This produces metadata conceptually similar to:
1<title>Next.js Course</title> 2 3<meta 4 name="description" 5 content="Learn Next.js, Server Components, APIs, authentication, and production architecture." 6/>
Metadata in layout.tsx
Layouts are useful for metadata that applies to multiple pages.
For example:
1app/ 2├── layout.tsx 3├── page.tsx 4└── courses/ 5 ├── page.tsx 6 └── [slug]/ 7 └── page.tsx
The root layout can define common metadata:
1import type { Metadata } from "next"; 2 3export const metadata: Metadata = { 4 title: { 5 default: "Tech3Space", 6 template: "%s | Tech3Space", 7 }, 8 9 description: 10 "Learn modern software development, web development, AI, and computer science.", 11};
Then a page can provide:
1export const metadata: Metadata = { 2 title: "Next.js Masterclass", 3};
The resulting title can follow the template:
1Next.js Masterclass | Tech3Space
Metadata Inheritance
Next.js supports metadata inheritance through layouts.
Conceptually:
1Root Layout 2 │ 3 ├── Common metadata 4 │ 5 └── Course Layout 6 │ 7 └── Course Page 8 ↓ 9 Page-specific metadata
This allows you to avoid repeating the same information on every page.
For example:
1Root 2├── Site name 3├── Default description 4└── Template 5 6Course page 7├── Course title 8└── Course description
SEO Title
The title is one of the most important metadata values.
Example:
1export const metadata: Metadata = { 2 title: "Next.js Masterclass", 3};
For dynamic pages:
1Next.js Masterclass | Tech3Space
A useful SEO title should clearly communicate the page topic.
Avoid titles like:
1Page 1 2Course 3Learn 4Home
Prefer:
1Next.js Masterclass — Complete Next.js Course
The exact title should reflect the actual page content.
Title Templates
A title template is useful for maintaining consistent branding.
1export const metadata: Metadata = { 2 title: { 3 default: "Tech3Space", 4 template: "%s | Tech3Space", 5 }, 6};
Now:
1export const metadata: Metadata = { 2 title: "Next.js Masterclass", 3};
can become:
1Next.js Masterclass | Tech3Space
This is useful for large applications containing many pages.
Meta Description
The description explains what the page contains.
Example:
1export const metadata: Metadata = { 2 description: 3 "Learn Next.js with Server Components, data fetching, APIs, authentication, and production architecture.", 4};
A practical SEO description should:
- Clearly describe the page
- Match the actual content
- Include relevant terminology naturally
- Give users a reason to visit
- Avoid keyword stuffing
For your course pages, a good description might be:
1Learn Next.js with Server Components, data fetching, APIs, authentication, TypeScript, and production application architecture.
Dynamic Metadata
Static metadata is not enough for dynamic pages.
Consider:
1/courses/nextjs 2/courses/react 3/courses/typescript
Each URL should have different metadata.
The architecture becomes:
1/courses/nextjs 2 ↓ 3slug = "nextjs" 4 ↓ 5Fetch course 6 ↓ 7generateMetadata() 8 ↓ 9Next.js SEO metadata
This is where generateMetadata() becomes important.
generateMetadata
For dynamic metadata, Next.js provides:
1generateMetadata()
Example:
1import type { Metadata } from "next"; 2 3type Props = { 4 params: Promise<{ 5 slug: string; 6 }>; 7}; 8 9export async function generateMetadata( 10 { params }: Props 11): Promise<Metadata> { 12 const { slug } = await params; 13 14 const course = await getCourse(slug); 15 16 return { 17 title: course.title, 18 description: course.description, 19 }; 20} 21 22export default async function CoursePage({ 23 params, 24}: Props) { 25 const { slug } = await params; 26 27 const course = await getCourse(slug); 28 29 return ( 30 <main> 31 <h1>{course.title}</h1> 32 </main> 33 ); 34}
The important flow is:
1URL 2 ↓ 3slug 4 ↓ 5getCourse(slug) 6 ↓ 7course data 8 ↓ 9generateMetadata() 10 ↓ 11SEO metadata
Dynamic Course Metadata
Imagine your database contains:
1{ 2 "slug": "nextjs-masterclass", 3 "title": "Next.js Masterclass", 4 "description": "Learn Next.js from fundamentals to production." 5}
The dynamic metadata can become:
1export async function generateMetadata( 2 { params }: Props 3): Promise<Metadata> { 4 const { slug } = await params; 5 6 const course = await getCourse(slug); 7 8 return { 9 title: course.title, 10 description: course.description, 11 }; 12}
For:
1/courses/nextjs-masterclass
the generated metadata could be:
1Title: 2Next.js Masterclass 3 4Description: 5Learn Next.js from fundamentals to production.
Dynamic SEO With Fallbacks
Database fields may sometimes be missing.
Avoid:
1return { 2 title: course.title, 3 description: course.description, 4};
if either field might be undefined and you have better fallback values available.
Instead:
1return { 2 title: 3 course.seoTitle ?? 4 course.title, 5 6 description: 7 course.seoDescription ?? 8 course.description, 9};
This gives you:
1SEO title 2 ↓ 3If missing 4 ↓ 5Course title
and:
1SEO description 2 ↓ 3If missing 4 ↓ 5Course description
SEO Fields in Your Database
For a large content platform, consider storing dedicated SEO fields:
1courses 2│ 3├── title 4├── description 5├── slug 6│ 7├── seo_title 8├── seo_description 9├── seo_image 10└── canonical_url
This allows editors to control search presentation independently from the main content.
For example:
1{ 2 "title": "Next.js Masterclass", 3 "description": "Complete course for learning Next.js.", 4 "seoTitle": "Next.js Masterclass — Complete Next.js Course", 5 "seoDescription": "Learn Next.js with Server Components, APIs, authentication, and production architecture." 6}
Open Graph
Open Graph metadata controls how pages can be represented when shared on platforms that support Open Graph.
Example:
1export const metadata: Metadata = { 2 title: "Next.js Masterclass", 3 4 description: 5 "Learn Next.js from fundamentals to production.", 6 7 openGraph: { 8 title: "Next.js Masterclass", 9 description: 10 "Learn Next.js from fundamentals to production.", 11 url: "https://example.com/courses/nextjs", 12 siteName: "Tech3Space", 13 images: [ 14 { 15 url: "https://example.com/images/nextjs-course.jpg", 16 width: 1200, 17 height: 630, 18 alt: "Next.js Masterclass", 19 }, 20 ], 21 type: "website", 22 }, 23};
Conceptually:
1Page 2 ↓ 3Open Graph Metadata 4 ↓ 5Social Platform 6 ↓ 7Title + Description + Image
Dynamic Open Graph Metadata
For dynamic course pages:
1export async function generateMetadata( 2 { params }: Props 3): Promise<Metadata> { 4 const { slug } = await params; 5 6 const course = await getCourse(slug); 7 8 return { 9 title: course.title, 10 11 description: 12 course.seoDescription ?? 13 course.description, 14 15 openGraph: { 16 title: course.title, 17 18 description: 19 course.seoDescription ?? 20 course.description, 21 22 images: [ 23 { 24 url: course.image, 25 width: 1200, 26 height: 630, 27 alt: course.title, 28 }, 29 ], 30 }, 31 }; 32}
Now every course can have its own social preview.
Twitter / X Metadata
Next.js also supports metadata for Twitter/X cards.
Example:
1export const metadata: Metadata = { 2 title: "Next.js Masterclass", 3 4 twitter: { 5 card: "summary_large_image", 6 title: "Next.js Masterclass", 7 description: 8 "Learn Next.js from fundamentals to production.", 9 images: [ 10 "https://example.com/images/nextjs-course.jpg", 11 ], 12 }, 13};
The important information is:
1Card type 2Title 3Description 4Image
Canonical URLs
Canonical URLs are important when multiple URLs can represent the same content.
Suppose the same course is accessible through:
1/courses/nextjs 2/courses/nextjs/ 3/courses/nextjs?ref=google
You generally want search engines to understand the preferred URL.
Example:
1export const metadata: Metadata = { 2 alternates: { 3 canonical: 4 "https://example.com/courses/nextjs", 5 }, 6};
For dynamic pages:
1export async function generateMetadata( 2 { params }: Props 3): Promise<Metadata> { 4 const { slug } = await params; 5 6 const course = await getCourse(slug); 7 8 return { 9 title: course.title, 10 11 alternates: { 12 canonical: 13 `https://example.com/courses/${course.slug}`, 14 }, 15 }; 16}
Architecture:
1Multiple URLs 2 ↓ 3Preferred URL 4 ↓ 5Canonical 6 ↓ 7Search engine understands 8preferred version
Canonical URLs and Site Architecture
For a large content platform, establish a consistent URL structure.
For example:
1/courses/nextjs 2/courses/react 3/courses/typescript
Then canonical URLs should follow the same structure.
Avoid constantly changing URL structures unless there is a strong reason.
If URLs must change, use appropriate redirects and update:
1Canonical 2Sitemap 3Internal links 4Metadata
Robots Metadata
Robots metadata controls whether search engines should index or follow a page.
For example:
1export const metadata: Metadata = { 2 robots: { 3 index: false, 4 follow: false, 5 }, 6};
This can be useful for pages such as:
1Private dashboard 2Internal tools 3Temporary pages 4Certain account pages
For a normal public course page, you generally want it indexable:
1export const metadata: Metadata = { 2 robots: { 3 index: true, 4 follow: true, 5 }, 6};
However, you usually do not need to explicitly specify this on every normal public page because index/follow is generally the default.
Public vs Private Pages
Think about SEO as an architectural decision.
1Public Course 2 ↓ 3Indexable 4 ↓ 5Search Engine 6 7Private Dashboard 8 ↓ 9No Index 10 ↓ 11Search Engine should not be used
For example:
1/courses/nextjs 2 ↓ 3Public 4 ↓ 5SEO 6 7/dashboard 8 ↓ 9Private 10 ↓ 11No indexing
But remember:
Robots metadata is not an authentication mechanism.
This is extremely important.
Do not try to protect private data using:
1robots: { 2 index: false, 3}
Authentication and authorization must protect the actual route.
Metadata and Authentication
A protected page should be protected by application logic:
1User 2 ↓ 3Authentication 4 ↓ 5Authorization 6 ↓ 7Protected Page
not:
1User 2 ↓ 3robots: noindex 4 ↓ 5Protected
noindex only communicates indexing preferences to compliant crawlers. It does not prevent someone who has access to the URL from requesting it.
Metadata for a Course Page
A production course page might use:
1import type { Metadata } from "next"; 2 3type Props = { 4 params: Promise<{ 5 slug: string; 6 }>; 7}; 8 9export async function generateMetadata( 10 { params }: Props 11): Promise<Metadata> { 12 const { slug } = await params; 13 14 const course = await getCourse(slug); 15 16 const title = 17 course.seoTitle ?? 18 course.title; 19 20 const description = 21 course.seoDescription ?? 22 course.description; 23 24 const canonical = 25 `https://example.com/courses/${course.slug}`; 26 27 return { 28 title, 29 30 description, 31 32 alternates: { 33 canonical, 34 }, 35 36 openGraph: { 37 title, 38 description, 39 url: canonical, 40 siteName: "Tech3Space", 41 images: course.image 42 ? [ 43 { 44 url: course.image, 45 width: 1200, 46 height: 630, 47 alt: course.title, 48 }, 49 ] 50 : undefined, 51 type: "website", 52 }, 53 54 twitter: { 55 card: "summary_large_image", 56 title, 57 description, 58 images: course.image 59 ? [course.image] 60 : undefined, 61 }, 62 63 robots: { 64 index: true, 65 follow: true, 66 }, 67 }; 68}
This creates a complete SEO metadata layer.
Handle Missing Courses
Dynamic metadata should also handle missing content.
For example:
1import { notFound } from "next/navigation"; 2 3export async function generateMetadata( 4 { params }: Props 5): Promise<Metadata> { 6 const { slug } = await params; 7 8 const course = await getCourse(slug); 9 10 if (!course) { 11 notFound(); 12 } 13 14 return { 15 title: course.seoTitle ?? course.title, 16 description: 17 course.seoDescription ?? 18 course.description, 19 }; 20}
The page itself should also handle the missing resource appropriately.
The architecture is:
1Request 2 ↓ 3Find Course 4 ↓ 5Course exists? 6 ├── Yes → Metadata + Page 7 │ 8 └── No → 404
Do not generate misleading SEO metadata for a page that does not actually exist.
SEO Metadata Architecture
A scalable content platform might use:
1Course Database 2│ 3├── title 4├── description 5├── slug 6├── image 7├── seoTitle 8└── seoDescription 9 ↓ 10generateMetadata() 11 ↓ 12┌───────────────────────┐ 13│ Title │ 14│ Description │ 15│ Canonical │ 16│ Open Graph │ 17│ Twitter/X │ 18│ Robots │ 19└───────────────────────┘ 20 ↓ 21Search Engines 22 + 23Social Platforms
This keeps SEO data close to the content it describes.
Metadata for Blog Pages
The same pattern works for blog articles.
Suppose:
1/blog/nextjs-server-components
The database contains:
1{ 2 "title": "Understanding Next.js Server Components", 3 "description": "Learn how Server Components work in Next.js.", 4 "slug": "nextjs-server-components" 5}
Then:
1export async function generateMetadata( 2 { params }: Props 3): Promise<Metadata> { 4 const { slug } = await params; 5 6 const post = await getPost(slug); 7 8 if (!post) { 9 notFound(); 10 } 11 12 return { 13 title: post.title, 14 15 description: post.description, 16 17 alternates: { 18 canonical: 19 `https://example.com/blog/${post.slug}`, 20 }, 21 }; 22}
The architecture is reusable:
1Course 2 ↓ 3generateMetadata() 4 5Blog 6 ↓ 7generateMetadata() 8 9Documentation 10 ↓ 11generateMetadata() 12 13Formula 14 ↓ 15generateMetadata()
Metadata for Documentation
Documentation pages can also have dynamic metadata.
For example:
1/docs/nextjs/server-components
could generate:
1Title: 2Next.js Server Components Guide 3 4Description: 5Learn how Server Components work in the Next.js App Router.
This is especially useful for large documentation websites where thousands of pages may exist.
Metadata vs Content
Do not confuse metadata with visible page content.
Metadata:
1<title> 2<meta name="description"> 3Open Graph 4Canonical 5Robots
Visible content:
1<h1> 2<p> 3<h2> 4Images 5Code examples 6Tables
A strong SEO page needs both.
1Technical SEO 2 + 3Useful Content 4 + 5Good Page Architecture 6 ↓ 7High-quality page
Do not create metadata that promises content the page does not contain.
SEO Title vs H1
The <title> and <h1> do different jobs.
For example:
1Browser/Search Title: 2Next.js Server Components — Complete Guide 3 4Page H1: 5Next.js Server Components
The title is primarily used for the document/search presentation.
The H1 is visible page content.
They can be similar, but they do not have to be identical.
Keyword Strategy
Do not create titles like:
1Next.js, Next.js Course, Next.js Tutorial, 2Learn Next.js, Next.js Training, Next.js Guide
This is keyword stuffing.
Prefer natural language:
1Next.js Masterclass — Complete Next.js Course
And a natural description:
1Learn Next.js with Server Components, data fetching, 2APIs, authentication, TypeScript, and production architecture.
The goal is:
1Relevant keyword 2 + 3Natural language 4 + 5Useful information
Common Mistakes
Mistake 1 — Using the Same Metadata Everywhere
Bad:
1export const metadata: Metadata = { 2 title: "Tech3Space", 3 description: "Learn technology.", 4};
on every dynamic page.
Instead, use dynamic metadata for dynamic content.
Mistake 2 — Forgetting generateMetadata
For:
1/courses/[slug]
static metadata cannot describe every course correctly.
Use:
1generateMetadata()
to generate page-specific metadata.
Mistake 3 — Missing Canonical URLs
Dynamic content with multiple URL variations can create canonicalization problems.
Define a consistent canonical strategy.
1alternates: { 2 canonical: canonicalUrl, 3}
Mistake 4 — Treating noindex as Security
This is incorrect:
1noindex 2 ≠ 3authentication
Private content requires actual authorization.
Mistake 5 — Missing Social Metadata
A page may have good search metadata but still have a poor social preview.
For important public pages, consider:
1Open Graph 2Twitter/X 3Image 4Title 5Description
Mistake 6 — Metadata Does Not Match Content
Bad:
1Title: 2Best Python Course 3 4Page: 5Next.js tutorial
Metadata should accurately represent the page.
Module 25 Learning Checklist
After completing this module, you should understand:
- Metadata API
- Static metadata
- Dynamic metadata
generateMetadata- SEO titles
- Meta descriptions
- Title templates
- Metadata inheritance
- Open Graph
- Twitter/X metadata
- Canonical URLs
- Robots metadata
- Public vs private SEO
- Dynamic course metadata
- Dynamic blog metadata
- Dynamic documentation metadata
- SEO fallbacks
- Missing content handling
- Metadata and authentication
- SEO-friendly architecture
- Keyword stuffing avoidance
- Search and social metadata
Final Mental Model
The most important architecture is:
1Dynamic URL 2 ↓ 3Route Parameter 4 ↓ 5Fetch Content 6 ↓ 7generateMetadata() 8 ↓ 9┌───────────────────────┐ 10│ SEO Title │ 11│ Description │ 12│ Canonical URL │ 13│ Open Graph │ 14│ Twitter/X │ 15│ Robots │ 16└───────────────────────┘ 17 ↓ 18Search Engine 19 + 20Social Platform
For a production course platform:
1Course Database 2 ↓ 3 slug 4 ↓ 5getCourse(slug) 6 ↓ 7generateMetadata() 8 ↓ 9┌──────────────────────────┐ 10│ Course SEO Title │ 11│ Course Description │ 12│ Canonical URL │ 13│ Course Image │ 14│ Open Graph │ 15│ Twitter/X │ 16└──────────────────────────┘ 17 ↓ 18Course Page
The key lesson is:
Next.js Metadata API lets you build SEO into your application architecture rather than manually managing
<head>tags on every page. Static pages can use static metadata, while dynamic content such as courses, blogs, and documentation should usegenerateMetadata()to produce accurate page-specific SEO metadata.