🚀 SEO, Performance & Deploy · Lesson 27
Metadata & SEO
What Google needs from a page
- Title — the blue link; most important for clicks.
- Description — the grey text under it.
- Canonical URL — which URL is the "real" one.
- Structured data — extra details (price, rating, FAQ).
Static metadata
JSX – export const metadata
// app/courses/page.jsx (EduSite)
export const metadata = {
title: 'All Courses',
description: 'Coding courses in C, Python, Web Development, Next.js, Android and MySQL with fees and duration.',
alternates: { canonical: '/courses' },
}Generated HTML in <head>
<title>All Courses | EduSite</title>
<meta name="description" content="Coding courses in C, Python, Web Development, …">
<link rel="canonical" href="https://edusite.in/courses">
Title templates in the root layout
JSX – app/layout.jsx
// app/layout.jsx (EduSite)
import { site } from '@/lib/site'
export const metadata = {
metadataBase: new URL(site.url), // makes relative URLs absolute
title: {
default: `${site.name} – ${site.tagline}`, // used when a page sets no title
template: `%s | ${site.name}`, // "All Courses | EduSite"
},
description: site.description,
alternates: { canonical: '/' },
openGraph: { type: 'website', siteName: site.name, locale: 'en_IN' },
twitter: { card: 'summary_large_image' },
robots: { index: true, follow: true },
}
export const viewport = { themeColor: '#4f46e5' }generateMetadata for dynamic pages
JSX – generateMetadata
// app/courses/[slug]/page.jsx (EduSite)
export async function generateMetadata({ params }) {
const { slug } = await params
const course = getCourse(slug)
if (!course) return { title: 'Course not found' }
return {
title: `${course.title} Course (${course.duration})`,
description: `${course.summary} Fee ${rupees(course.fee)}. Level: ${course.level}.`,
alternates: { canonical: `/courses/${course.slug}` },
openGraph: { images: [course.image] },
}
}Generated HTML for /courses/nextjs
<title>Next.js Course (6 weeks) | EduSite</title>
<meta name="description" content="React + Next.js App Router, APIs, database, SEO and deployment. Fee ₹4,499. Level: Intermediate.">
<link rel="canonical" href="https://edusite.in/courses/nextjs">
<meta property="og:image" content="https://edusite.in/images/nextjs.png">
Canonical URLs and metadataBase
The same page can be reached at /courses, /courses?ref=fb and /courses?q=. A canonical tag tells Google which one to index so ranking is not split. With metadataBase set once, you can write short paths like '/courses'.
Open Graph and social images
When someone shares your link on WhatsApp, Facebook or LinkedIn, the OG image and title appear. A file named opengraph-image.jsx generates that image automatically.
JSX – app/opengraph-image.jsx
// app/opengraph-image.jsx (EduSite) — generates a 1200×630 PNG
import { ImageResponse } from 'next/og'
import { site } from '@/lib/site'
export const alt = `${site.name} – ${site.tagline}`
export const size = { width: 1200, height: 630 }
export const contentType = 'image/png'
export default function OpengraphImage() {
return new ImageResponse(
(
<div style={{ width: '100%', height: '100%', display: 'flex', flexDirection: 'column',
justifyContent: 'center', padding: 80,
background: 'linear-gradient(135deg,#4f46e5,#0f172a)', color: '#fff' }}>
<div style={{ fontSize: 88, fontWeight: 800 }}>{site.name}</div>
<div style={{ fontSize: 44, marginTop: 16, opacity: 0.9 }}>{site.tagline}</div>
</div>
),
size
)
}JSON-LD structured data
JSX – components/JsonLd.jsx + usage
// components/JsonLd.jsx (EduSite)
export default function JsonLd({ data }) {
return (
<script type="application/ld+json"
dangerouslySetInnerHTML={{ __html: JSON.stringify(data).replace(/</g, '\\u003c') }} />
)
}
// app/courses/[slug]/page.jsx — Course schema
<JsonLd data={{
'@context': 'https://schema.org',
'@type': 'Course',
name: course.title,
description: course.summary,
provider: { '@type': 'Organization', name: site.name, sameAs: site.url },
offers: { '@type': 'Offer', price: course.fee, priceCurrency: 'INR', category: 'Paid' },
}} />
// app/blog/[slug]/page.jsx — BlogPosting schema
<JsonLd data={{
'@context': 'https://schema.org',
'@type': 'BlogPosting',
headline: post.title,
datePublished: post.date,
author: { '@type': 'Organization', name: site.name },
}} />Writing titles that get clicks
| ❌ Weak | ✅ Strong |
|---|---|
| Next.js | Next.js Course (6 weeks) – Fee ₹4,499 | EduSite |
| Blog post 1 | How to Start Coding in 2026: 30-Day Plan for Beginners |
| Contact | Contact Us – Book a Free Demo Class |
Put the main keyword first, add a number or benefit, keep under ~60 characters, and write a description that ends with a reason to click.
How to check your SEO
- 1View page source (Ctrl+U) → check
, description, canonical - 2Google Rich Results Test → validate JSON-LD
- 3Lighthouse → SEO score should be 100
- 4Google Search Console → submit sitemap, watch impressions and CTR
Common mistakes
- Same title on every page → Google rewrites them and CTR drops.
- Missing
metadataBase→ OG image URLs are relative and previews break. - Putting
'use client'on page.jsx → metadata export stops working. - JSON-LD that does not match visible content → can cause a manual penalty.
Summary
metadatafor static pages,generateMetadatafor dynamic ones.- Root layout:
metadataBase+ title template + defaults. - Canonical, OG image and JSON-LD complete the SEO picture.
✅ Practice
Give every page a unique title/description, add Course JSON-LD to course pages and test one URL in Google's Rich Results Test.
Frequently Asked Questions
Can I use metadata in a Client Component?
No. The metadata export and generateMetadata only work in Server Components (page.jsx and layout.jsx). Keep pages as Server Components and move interactivity into child components.
What length should a title and description be?
Aim for titles under about 60 characters and descriptions around 140–160 characters so Google shows them fully in search results.
Is JSON-LD required for ranking?
It is not a direct ranking factor, but it helps Google understand the page and can produce rich results (FAQ, course, breadcrumb), which increases clicks.
