Back to Blog
7 min read

Next.js SEO Optimization: Complete Guide to Ranking Higher

Master SEO in Next.js with metadata optimization, structured data, Core Web Vitals, sitemaps, and advanced techniques for better search engine rankings.

#Next.js#SEO#React#Web Development#Performance
Next.js SEO Optimization: Complete Guide to Ranking Higher

SEO is crucial for any website's success. Next.js provides powerful tools for building SEO-friendly applications. This guide covers everything from basic metadata to advanced optimization techniques.

Metadata API in Next.js 15#

Static Metadata#

// app/page.tsx
import type { Metadata } from 'next';
 
export const metadata: Metadata = {
  title: 'Home | My Website',
  description: 'Welcome to my website. We offer the best products and services.',
  keywords: ['products', 'services', 'quality'],
  authors: [{ name: 'John Doe', url: 'https://johndoe.com' }],
  creator: 'John Doe',
  publisher: 'My Company',
  robots: {
    index: true,
    follow: true,
    googleBot: {
      index: true,
      follow: true,
      'max-video-preview': -1,
      'max-image-preview': 'large',
      'max-snippet': -1,
    },
  },
  openGraph: {
    type: 'website',
    locale: 'en_US',
    url: 'https://mywebsite.com',
    siteName: 'My Website',
    title: 'Home | My Website',
    description: 'Welcome to my website',
    images: [
      {
        url: 'https://mywebsite.com/og-image.jpg',
        width: 1200,
        height: 630,
        alt: 'My Website',
      },
    ],
  },
  twitter: {
    card: 'summary_large_image',
    title: 'Home | My Website',
    description: 'Welcome to my website',
    creator: '@johndoe',
    images: ['https://mywebsite.com/twitter-image.jpg'],
  },
  alternates: {
    canonical: 'https://mywebsite.com',
    languages: {
      'en-US': 'https://mywebsite.com/en',
      'fa-IR': 'https://mywebsite.com/fa',
    },
  },
};
 
export default function HomePage() {
  return <main>...</main>;
}

Always include Open Graph and Twitter Card metadata for better social media sharing previews.

Dynamic Metadata#

// app/blog/[slug]/page.tsx
import type { Metadata } from 'next';
import { getPostBySlug } from '@/lib/posts';
 
interface Props {
  params: { slug: string };
}
 
export async function generateMetadata({ params }: Props): Promise<Metadata> {
  const post = await getPostBySlug(params.slug);
 
  if (!post) {
    return {
      title: 'Post Not Found',
    };
  }
 
  return {
    title: `${post.title} | My Blog`,
    description: post.excerpt,
    authors: [{ name: post.author.name }],
    openGraph: {
      type: 'article',
      title: post.title,
      description: post.excerpt,
      url: `https://mywebsite.com/blog/${params.slug}`,
      publishedTime: post.publishedAt,
      modifiedTime: post.updatedAt,
      authors: [post.author.name],
      images: [
        {
          url: post.coverImage,
          width: 1200,
          height: 630,
          alt: post.title,
        },
      ],
      tags: post.tags,
    },
    twitter: {
      card: 'summary_large_image',
      title: post.title,
      description: post.excerpt,
      images: [post.coverImage],
    },
  };
}

Metadata Template#

// app/layout.tsx
import type { Metadata } from 'next';
 
export const metadata: Metadata = {
  metadataBase: new URL('https://mywebsite.com'),
  title: {
    default: 'My Website',
    template: '%s | My Website', // Dynamic pages will use this template
  },
  description: 'Default description for my website',
  openGraph: {
    type: 'website',
    siteName: 'My Website',
  },
};

Structured Data (JSON-LD)#

Organization Schema#

// components/StructuredData.tsx
export function OrganizationSchema() {
  const schema = {
    '@context': 'https://schema.org',
    '@type': 'Organization',
    name: 'My Company',
    url: 'https://mywebsite.com',
    logo: 'https://mywebsite.com/logo.png',
    sameAs: [
      'https://twitter.com/mycompany',
      'https://linkedin.com/company/mycompany',
      'https://github.com/mycompany',
    ],
    contactPoint: {
      '@type': 'ContactPoint',
      telephone: '+1-555-555-5555',
      contactType: 'customer service',
      availableLanguage: ['English', 'Persian'],
    },
  };
 
  return (
    <script
      type="application/ld+json"
      dangerouslySetInnerHTML={{ __html: JSON.stringify(schema) }}
    />
  );
}

Article Schema#

// components/ArticleSchema.tsx
interface ArticleSchemaProps {
  title: string;
  description: string;
  image: string;
  datePublished: string;
  dateModified: string;
  author: {
    name: string;
    url: string;
  };
  url: string;
}
 
export function ArticleSchema({
  title,
  description,
  image,
  datePublished,
  dateModified,
  author,
  url,
}: ArticleSchemaProps) {
  const schema = {
    '@context': 'https://schema.org',
    '@type': 'Article',
    headline: title,
    description: description,
    image: image,
    datePublished: datePublished,
    dateModified: dateModified,
    author: {
      '@type': 'Person',
      name: author.name,
      url: author.url,
    },
    publisher: {
      '@type': 'Organization',
      name: 'My Website',
      logo: {
        '@type': 'ImageObject',
        url: 'https://mywebsite.com/logo.png',
      },
    },
    mainEntityOfPage: {
      '@type': 'WebPage',
      '@id': url,
    },
  };
 
  return (
    <script
      type="application/ld+json"
      dangerouslySetInnerHTML={{ __html: JSON.stringify(schema) }}
    />
  );
}

Product Schema for E-commerce#

export function ProductSchema({ product }) {
  const schema = {
    '@context': 'https://schema.org',
    '@type': 'Product',
    name: product.name,
    description: product.description,
    image: product.images,
    sku: product.sku,
    brand: {
      '@type': 'Brand',
      name: product.brand,
    },
    offers: {
      '@type': 'Offer',
      url: `https://mywebsite.com/products/${product.slug}`,
      priceCurrency: 'USD',
      price: product.price,
      availability: product.inStock
        ? 'https://schema.org/InStock'
        : 'https://schema.org/OutOfStock',
      seller: {
        '@type': 'Organization',
        name: 'My Store',
      },
    },
    aggregateRating: product.reviews.length > 0 ? {
      '@type': 'AggregateRating',
      ratingValue: product.averageRating,
      reviewCount: product.reviews.length,
    } : undefined,
  };
 
  return (
    <script
      type="application/ld+json"
      dangerouslySetInnerHTML={{ __html: JSON.stringify(schema) }}
    />
  );
}

Sitemap Generation#

Dynamic Sitemap#

// app/sitemap.ts
import { MetadataRoute } from 'next';
import { getAllPosts } from '@/lib/posts';
import { getAllProducts } from '@/lib/products';
 
export default async function sitemap(): Promise<MetadataRoute.Sitemap> {
  const baseUrl = 'https://mywebsite.com';
 
  // Static pages
  const staticPages = [
    {
      url: baseUrl,
      lastModified: new Date(),
      changeFrequency: 'daily' as const,
      priority: 1,
    },
    {
      url: `${baseUrl}/about`,
      lastModified: new Date(),
      changeFrequency: 'monthly' as const,
      priority: 0.8,
    },
    {
      url: `${baseUrl}/contact`,
      lastModified: new Date(),
      changeFrequency: 'monthly' as const,
      priority: 0.8,
    },
  ];
 
  // Blog posts
  const posts = await getAllPosts();
  const blogPages = posts.map((post) => ({
    url: `${baseUrl}/blog/${post.slug}`,
    lastModified: new Date(post.updatedAt),
    changeFrequency: 'weekly' as const,
    priority: 0.7,
  }));
 
  // Products
  const products = await getAllProducts();
  const productPages = products.map((product) => ({
    url: `${baseUrl}/products/${product.slug}`,
    lastModified: new Date(product.updatedAt),
    changeFrequency: 'daily' as const,
    priority: 0.9,
  }));
 
  return [...staticPages, ...blogPages, ...productPages];
}

Robots.txt#

// app/robots.ts
import { MetadataRoute } from 'next';
 
export default function robots(): MetadataRoute.Robots {
  return {
    rules: [
      {
        userAgent: '*',
        allow: '/',
        disallow: ['/api/', '/admin/', '/private/'],
      },
      {
        userAgent: 'Googlebot',
        allow: '/',
        disallow: '/api/',
      },
    ],
    sitemap: 'https://mywebsite.com/sitemap.xml',
  };
}

Core Web Vitals Optimization#

Image Optimization#

import Image from 'next/image';
 
// Optimized image component
export function OptimizedImage({ src, alt, priority = false }) {
  return (
    <Image
      src={src}
      alt={alt}
      width={800}
      height={600}
      priority={priority} // Set true for above-the-fold images
      placeholder="blur"
      blurDataURL="data:image/jpeg;base64,/9j/4AAQSkZJRg..."
      sizes="(max-width: 768px) 100vw, (max-width: 1200px) 50vw, 33vw"
      className="object-cover"
    />
  );
}
 
// Hero image with priority loading
export function HeroSection() {
  return (
    <section>
      <Image
        src="/hero.jpg"
        alt="Hero image"
        fill
        priority // Critical for LCP
        sizes="100vw"
        className="object-cover"
      />
    </section>
  );
}

Font Optimization#

// app/layout.tsx
import { Inter, Vazirmatn } from 'next/font/google';
 
const inter = Inter({
  subsets: ['latin'],
  display: 'swap',
  variable: '--font-inter',
});
 
const vazirmatn = Vazirmatn({
  subsets: ['arabic'],
  display: 'swap',
  variable: '--font-vazirmatn',
});
 
export default function RootLayout({ children }) {
  return (
    <html className={`${inter.variable} ${vazirmatn.variable}`}>
      <body>{children}</body>
    </html>
  );
}

Script Optimization#

import Script from 'next/script';
 
export function Analytics() {
  return (
    <>
      {/* Load after page is interactive */}
      <Script
        src="https://www.googletagmanager.com/gtag/js?id=GA_ID"
        strategy="afterInteractive"
      />
      <Script id="google-analytics" strategy="afterInteractive">
        {`
          window.dataLayer = window.dataLayer || [];
          function gtag(){dataLayer.push(arguments);}
          gtag('js', new Date());
          gtag('config', 'GA_ID');
        `}
      </Script>
 
      {/* Load when browser is idle */}
      <Script
        src="https://third-party-script.js"
        strategy="lazyOnload"
      />
    </>
  );
}

Internationalization SEO#

Hreflang Tags#

// app/[locale]/layout.tsx
import type { Metadata } from 'next';
 
export async function generateMetadata({ params }): Promise<Metadata> {
  const { locale } = params;
  
  return {
    alternates: {
      canonical: `https://mywebsite.com/${locale}`,
      languages: {
        'en': 'https://mywebsite.com/en',
        'fa': 'https://mywebsite.com/fa',
        'x-default': 'https://mywebsite.com/en',
      },
    },
  };
}

Localized Sitemap#

// app/sitemap.ts
export default async function sitemap(): Promise<MetadataRoute.Sitemap> {
  const locales = ['en', 'fa'];
  const baseUrl = 'https://mywebsite.com';
 
  const pages = ['', '/about', '/contact', '/blog'];
 
  return pages.flatMap((page) =>
    locales.map((locale) => ({
      url: `${baseUrl}/${locale}${page}`,
      lastModified: new Date(),
      alternates: {
        languages: Object.fromEntries(
          locales.map((l) => [l, `${baseUrl}/${l}${page}`])
        ),
      },
    }))
  );
}

Performance Monitoring#

Web Vitals Tracking#

// app/components/WebVitals.tsx
'use client';
 
import { useReportWebVitals } from 'next/web-vitals';
 
export function WebVitals() {
  useReportWebVitals((metric) => {
    // Send to analytics
    const body = JSON.stringify({
      name: metric.name,
      value: metric.value,
      rating: metric.rating,
      delta: metric.delta,
      id: metric.id,
    });
 
    // Use sendBeacon for reliability
    if (navigator.sendBeacon) {
      navigator.sendBeacon('/api/analytics/vitals', body);
    } else {
      fetch('/api/analytics/vitals', {
        body,
        method: 'POST',
        keepalive: true,
      });
    }
  });
 
  return null;
}

Conclusion#

SEO in Next.js is about combining technical optimization with great content. By implementing proper metadata, structured data, sitemaps, and Core Web Vitals optimization, you can significantly improve your search engine rankings.

Key takeaways:

  • Use the Metadata API for comprehensive meta tags
  • Implement structured data for rich search results
  • Generate dynamic sitemaps for all content
  • Optimize images, fonts, and scripts for Core Web Vitals
  • Handle internationalization with proper hreflang tags
Next.js Metadata Documentation
0 views
More Articles