Metadata & SEO: Get Found on Google
Your app could be the best in the world, but if Google can't understand it, nobody finds it. Next.js gives you powerful tools to control exactly how your pages appear in search results, social media shares, and browser tabs.
Static Metadata
The simplest approach. Export a metadata object from any layout or page. It gets merged with parent metadata automatically.
1import type { Metadata } from "next"23export const metadata: Metadata = {4title: "My Blog | Prasen",5description: "Thoughts on web development, React, and Next.js",6keywords: ["blog", "react", "nextjs", "web development"],7authors: [{ name: "Prasen", url: "https://prasen.dev" }],8openGraph: {9 title: "My Blog | Prasen",10 description: "Thoughts on web development",11 url: "https://prasen.dev/blog",12 siteName: "Prasen's Blog",13 images: [14 {15 url: "/og-image.png",16 width: 1200,17 height: 630,18 alt: "Blog preview image",19 },20 ],21 type: "website",22},23twitter: {24 card: "summary_large_image",25 title: "My Blog | Prasen",26 description: "Thoughts on web development",27 images: ["/og-image.png"],28},29}3031export default function BlogPage() {32return <main>...</main>33}Dynamic Metadata
For pages with dynamic content (blog posts, products), use generateMetadata. It receives the same params as your page component.
1import type { Metadata } from "next"23interface Props {4params: Promise<{ slug: string }>5}67export async function generateMetadata({ params }: Props): Promise<Metadata> {8const { slug } = await params9const post = await getPost(slug)1011return {12 title: post.title,13 description: post.excerpt,14 openGraph: {15 title: post.title,16 description: post.excerpt,17 images: [post.coverImage],18 },19}20}2122export default async function PostPage({ params }: Props) {23const { slug } = await params24const post = await getPost(slug)25return <article>{post.content}</article>26}Metadata Merging Rules
Next.js automatically merges metadata from parent layouts to child pages. Child values override parent values. This means you set defaults in your root layout and override per-page as needed.
Template titles
⢠Root layout: title: { template: '%s | My Site', default: 'My Site' }
⢠Child page: title: 'About' renders as 'About | My Site'
⢠Keeps your brand consistent without repeating the suffix everywhere
Sitemaps & robots.txt
Next.js can generate these automatically. Create a sitemap.ts and robots.ts file in your app directory.
1import type { MetadataRoute } from "next"23export default function sitemap(): MetadataRoute.Sitemap {4const posts = await getAllPosts()56return [7 { url: "https://prasen.dev", lastModified: new Date() },8 { url: "https://prasen.dev/blog", lastModified: new Date() },9 ...posts.map((post) => ({10 url: `https://prasen.dev/blog/${post.slug}`,11 lastModified: post.updatedAt,12 })),13]14}1import type { MetadataRoute } from "next"23export default function robots(): MetadataRoute.Robots {4return {5 rules: {6 userAgent: "*",7 allow: "/",8 disallow: ["/api/", "/admin/"],9 },10 sitemap: "https://prasen.dev/sitemap.xml",11}12}JSON-LD Structured Data
For rich search results (star ratings, FAQ snippets, breadcrumbs), add JSON-LD structured data to your pages.
1export default function BlogPost({ post }) {2const jsonLd = {3 "@context": "https://schema.org",4 "@type": "BlogPosting",5 headline: post.title,6 author: {7 "@type": "Person",8 name: "Prasen",9 url: "https://prasen.dev",10 },11 datePublished: post.publishedAt,12 image: post.coverImage,13}1415return (16 <>17 <script18 type="application/ld+json"19 dangerouslySetInnerHTML={{ __html: JSON.stringify(jsonLd) }}20 />21 <article>{post.content}</article>22 </>23)24}SEO checklist for every page
⢠Unique title and description
⢠Open Graph image (1200x630px)
⢠Proper heading hierarchy (h1 > h2 > h3)
⢠Alt text on all images
⢠Fast load time (LCP < 2.5s)
⢠Mobile-friendly layout
⢠Canonical URL if content is duplicated