SEO & Metadata
Optimize your Next.js application for search engines with ready-to-use code snippets.
Next.js includes a Metadata API that allows you to define metadata to improve your application's SEO. This guide includes ready-to-copy examples for your Next.js project.
Below is a complete example of the metadata setup, so you can easily copy it into your app/layout.jsx
export const metadata = { metadataBase: new URL("https://yourdomain.com"), title: { default: "My Application - Main Tagline", template: "%s | My Application", }, description: "The best web application for your needs. Detailed description of what you offer.", keywords: ["nextjs", "react", "web app", "seo", "typescript"], authors: [{ name: "Your Name", url: "https://yourdomain.com" }], creator: "Your Name", publisher: "Your Company", // Open Graph openGraph: { title: "My Application", description: "The best web application for your needs", url: "https://yourdomain.com", siteName: "My Application", images: [ { url: "/og-image.jpg", width: 1200, height: 630, alt: "My Application Preview", }, ], locale: "en_US", type: "website", }, // Twitter twitter: { card: "summary_large_image", title: "My Application", description: "The best web application for your needs", creator: "@yourusername", images: ["/twitter-image.jpg"], }, // Robots robots: { index: true, follow: true, googleBot: { index: true, follow: true, "max-video-preview": -1, "max-image-preview": "large", "max-snippet": -1, }, }, // Icons icons: { icon: "/icon.png", shortcut: "/shortcut-icon.png", apple: "/apple-icon.png", }, // Verification verification: { google: "your-google-verification-code", }, // Alternates alternates: { canonical: "https://yourdomain.com", },};export const metadata = { metadataBase: new URL("https://yourdomain.com"), title: { default: "My Application - Main Tagline", template: "%s | My Application", }, description: "The best web application for your needs. Detailed description of what you offer.", keywords: ["nextjs", "react", "web app", "seo", "typescript"], authors: [{ name: "Your Name", url: "https://yourdomain.com" }], creator: "Your Name", publisher: "Your Company", // Open Graph openGraph: { title: "My Application", description: "The best web application for your needs", url: "https://yourdomain.com", siteName: "My Application", images: [ { url: "/og-image.jpg", width: 1200, height: 630, alt: "My Application Preview", }, ], locale: "en_US", type: "website", }, // Twitter twitter: { card: "summary_large_image", title: "My Application", description: "The best web application for your needs", creator: "@yourusername", images: ["/twitter-image.jpg"], }, // Robots robots: { index: true, follow: true, googleBot: { index: true, follow: true, "max-video-preview": -1, "max-image-preview": "large", "max-snippet": -1, }, }, // Icons icons: { icon: "/icon.png", shortcut: "/shortcut-icon.png", apple: "/apple-icon.png", }, // Verification verification: { google: "your-google-verification-code", }, // Alternates alternates: { canonical: "https://yourdomain.com", },};For pages with dynamic content (blogs, products, etc.), use generateMetadata()
export async function generateMetadata({ params }) { const { slug } = await params; // Fetch data const post = await fetch(`https://api.yourdomain.com/posts/${slug}`) .then((res) => res.json()); return { title: post.title, description: post.excerpt, openGraph: { title: post.title, description: post.excerpt, images: [ { url: post.image, width: 1200, height: 630, }, ], type: "article", publishedTime: post.publishedAt, authors: [post.author.name], }, twitter: { card: "summary_large_image", title: post.title, description: post.excerpt, images: [post.image], }, };}export default function BlogPost({ params }) { // Your component...}export async function generateMetadata({ params }) { const { slug } = await params; // Fetch data const post = await fetch(`https://api.yourdomain.com/posts/${slug}`) .then((res) => res.json()); return { title: post.title, description: post.excerpt, openGraph: { title: post.title, description: post.excerpt, images: [ { url: post.image, width: 1200, height: 630, }, ], type: "article", publishedTime: post.publishedAt, authors: [post.author.name], }, twitter: { card: "summary_large_image", title: post.title, description: post.excerpt, images: [post.image], }, };}export default function BlogPost({ params }) { // Your component...}Automatically generate Open Graph images using Next.js. Create app/opengraph-image.tsx or app/[route]/opengraph-image.tsx for dynamic routes. Next.js will automatically generate the image and add the correct meta tags.
import { ImageResponse } from 'next/og';// Image metadataexport const alt = 'My Application';export const size = { width: 1200, height: 630,};export const contentType = 'image/png';// Image generationexport default async function Image() { return new ImageResponse( ( <div style={{ fontSize: 128, background: 'white', width: '100%', height: '100%', display: 'flex', alignItems: 'center', justifyContent: 'center', }} > My Application </div> ), { ...size, } );}import { ImageResponse } from 'next/og';// Image metadataexport const alt = 'My Application';export const size = { width: 1200, height: 630,};export const contentType = 'image/png';// Image generationexport default async function Image() { return new ImageResponse( ( <div style={{ fontSize: 128, background: 'white', width: '100%', height: '100%', display: 'flex', alignItems: 'center', justifyContent: 'center', }} > My Application </div> ), { ...size, } );}import { ImageResponse } from 'next/og';export const alt = 'Blog Post';export const size = { width: 1200, height: 630,};export const contentType = 'image/png';export default async function Image({ params }) { const { slug } = await params; // Fetch post data const post = await fetch(`https://api.yourdomain.com/posts/${slug}`) .then((res) => res.json()); return new ImageResponse( ( <div style={{ fontSize: 48, background: 'white', width: '100%', height: '100%', display: 'flex', alignItems: 'center', justifyContent: 'center', }} > {post.title} </div> ), { ...size, } );}import { ImageResponse } from 'next/og';export const alt = 'Blog Post';export const size = { width: 1200, height: 630,};export const contentType = 'image/png';export default async function Image({ params }) { const { slug } = await params; // Fetch post data const post = await fetch(`https://api.yourdomain.com/posts/${slug}`) .then((res) => res.json()); return new ImageResponse( ( <div style={{ fontSize: 48, background: 'white', width: '100%', height: '100%', display: 'flex', alignItems: 'center', justifyContent: 'center', }} > {post.title} </div> ), { ...size, } );}Next.js automatically generates sitemap.xml and robots.txt files. Create app/sitemap.js and app/robots.js in your app directory. These files will be automatically served at /sitemap.xml and /robots.txt.
export default function sitemap() { return [ { url: "https://yourdomain.com", lastModified: new Date(), changeFrequency: "yearly", priority: 1, }, { url: "https://yourdomain.com/about", lastModified: new Date(), changeFrequency: "monthly", priority: 0.8, }, { url: "https://yourdomain.com/blog", lastModified: new Date(), changeFrequency: "weekly", priority: 0.5, }, ];}export default function sitemap() { return [ { url: "https://yourdomain.com", lastModified: new Date(), changeFrequency: "yearly", priority: 1, }, { url: "https://yourdomain.com/about", lastModified: new Date(), changeFrequency: "monthly", priority: 0.8, }, { url: "https://yourdomain.com/blog", lastModified: new Date(), changeFrequency: "weekly", priority: 0.5, }, ];}export default function robots() { return { rules: { userAgent: "*", allow: "/", disallow: ["/admin/", "/api/"], }, sitemap: "https://yourdomain.com/sitemap.xml", };}export default function robots() { return { rules: { userAgent: "*", allow: "/", disallow: ["/admin/", "/api/"], }, sitemap: "https://yourdomain.com/sitemap.xml", };}Additional SEO Tips
- OG Images: Use 1200x630px for Open Graph (Facebook, LinkedIn)
- Twitter Images: 1200x600px works best for Twitter Cards
- Description: Between 150-160 characters is ideal for Google
- Title: Maximum 60 characters to avoid truncation in results
- Keywords: 5-10 relevant keywords is sufficient