Topic 8 of 47
Metadata, SEO & Image Optimization
Overview
Next.js has first-class SEO support through the Metadata API and optimized <Image> component. Proper metadata (title, description, OG tags) is critical for search rankings, while next/image automatically handles lazy loading, WebP conversion, and responsive images.
Syntax
typescript
// Static metadata
export const metadata = {
title: 'DevNotes — Learn to Code',
description: 'Learn web development with real-world examples.',
keywords: ['Next.js', 'React', 'TypeScript'],
openGraph: {
title: 'DevNotes',
description: 'Learn web development with real-world examples.',
url: 'https://devnotes.in',
siteName: 'DevNotes',
images: [{ url: '/og-image.png', width: 1200, height: 630 }],
locale: 'en_IN',
type: 'website',
},
twitter: {
card: 'summary_large_image',
title: 'DevNotes',
creator: '@devnotes_in',
},
};
// Dynamic metadata (for product/blog pages)
export async function generateMetadata({ params }) {
const post = await getPost(params.slug);
return {
title: `${post.title} | DevNotes`,
description: post.excerpt,
openGraph: { images: [post.coverImage] },
};
}
// next/image — optimized images
import Image from 'next/image';
<Image
src="/hero.jpg" // or external URL
alt="Hero banner"
width={1200}
height={630}
priority // don't lazy load above-the-fold images
quality={85}
/>Common Pitfalls
- External image domains must be added to next.config.js under images.remotePatterns — Next.js blocks unknown external domains.
- Priority prop on <Image> prevents lazy loading — use it ONLY for above-the-fold images (LCP element).
- Interview tip: Core Web Vitals are real Google ranking factors. Next.js <Image> improves LCP; Server Components reduce TTI; next/font eliminates FOUT.
Real-World Example
Full metadata setup for an e-commerce product page:
example
typescript
// app/products/[slug]/page.tsx
import { Metadata } from 'next';
import Image from 'next/image';
// Structured data for Google Shopping / rich results
function ProductJsonLd({ product }: { product: Product }) {
return (
<script
type="application/ld+json"
dangerouslySetInnerHTML={{
__html: JSON.stringify({
'@context': 'https://schema.org',
'@type': 'Product',
name: product.name,
description: product.description,
image: product.images,
offers: {
'@type': 'Offer',
price: product.price,
priceCurrency: 'INR',
availability: product.inStock
? 'https://schema.org/InStock'
: 'https://schema.org/OutOfStock',
},
aggregateRating: {
'@type': 'AggregateRating',
ratingValue: product.avgRating,
reviewCount: product.reviewCount,
},
}),
}}
/>
);
}
export default async function ProductPage({ params }) {
const product = await getProduct(params.slug);
return (
<>
<ProductJsonLd product={product} />
<Image
src={product.images[0]}
alt={product.name}
width={600} height={600}
priority // hero image — no lazy loading
sizes="(max-width: 768px) 100vw, 50vw"
/>
</>
);
}