We use cookies to enhance your experience on the site
CodeWorlds

SEO and dynamic metadata

Search Engine Optimization (SEO) is a crucial aspect of every modern web application. Next.js offers built-in solutions that enable efficient metadata management and optimization for search engine crawlers. In this chapter, we will focus on implementing dynamic metadata and SEO best practices in Next.js applications.

SEO basics in Next.js

Next.js was designed with SEO in mind, offering server-side rendering (SSR) and static site generation (SSG), which makes content immediately available to search engine crawlers.

The importance of metadata

Metadata is a critical element of every SEO strategy, providing search engines with context and information about the page content. Key metadata includes:

  1. Page titles - Concise, unique, and descriptive titles increase CTR
  2. Meta descriptions - Short summaries of page content (150-160 characters)
  3. Canonical tags - Prevent content duplication issues
  4. Alternate language tags - Indicate alternative language versions of the page

The new Metadata API in Next.js 13+

Next.js 13 introduced a new, more intuitive API for managing metadata through files

layout.js
or
page.js
in the App Router.

Static metadata

The simplest way to define metadata is to use the export of the object

metadata
:

1// app/page.tsx
2import type { Metadata } from 'next';
3
4export const metadata: Metadata = {
5  title: 'Home Page | My Next.js Application',
6  description: 'Discover our modern application built with Next.js',
7  keywords: ['next.js', 'react', 'javascript', 'application'],
8  authors: [{ name: 'Jan Kowalski', url: 'https://jankowalski.pl' }],
9  creator: 'XYZ Developer Team',
10  publisher: 'XYZ Company',
11  robots: {
12    index: true,
13    follow: true,
14    nocache: true,
15    googleBot: {
16      index: true,
17      follow: true,
18      'max-image-preview': 'large',
19      'max-snippet': -1,
20    },
21  },
22};
23
24export default function HomePage() {
25  return (
26    <main>
27      <h1>Welcome to my Next.js application</h1>
28      {/* Page content... */}
29    </main>
30  );
31}

Dynamic metadata

In real applications, dynamic metadata is often needed, generated based on external data (e.g., article content, product details):

1// app/blog/[slug]/page.tsx
2import type { Metadata } from 'next';
3import { getBlogPost } from '@/lib/api';
4
5// The generateMetadata function is called on the server during rendering
6export async function generateMetadata({ params }: { params: Promise<{ slug: string }> }): Promise<Metadata> {
7  // Fetch data from the API or database
8  const post = await getBlogPost((await params).slug);
9  
10  // If the post doesn't exist, return basic metadata
11  if (!post) {
12    return {
13      title: 'Post not found',
14      description: 'Sorry, we could not find this article',
15    };
16  }
17  
18  return {
19    title: `${post.title} | Blog My Application`,
20    description: post.excerpt || `Read the article: ${post.title}`,
21    keywords: post.tags,
22    authors: [{ name: post.author.name, url: post.author.website }],
23    openGraph: {
24      type: 'article',
25      title: post.title,
26      description: post.excerpt,
27      publishedTime: post.publishedAt,
28      authors: post.author.website,
29      images: [
30        {
31          url: post.coverImage,
32          width: 1200,
33          height: 630,
34          alt: post.title,
35        },
36      ],
37    },
38    twitter: {
39      card: 'summary_large_image',
40      title: post.title,
41      description: post.excerpt,
42      images: [post.coverImage],
43    },
44  };
45}
46
47export default async function BlogPostPage({ params }: { params: Promise<{ slug: string }> }) {
48  const post = await getBlogPost((await params).slug);
49  
50  if (!post) {
51    return <div>Post not found</div>;
52  }
53  
54  return (
55    <article>
56      <h1>{post.title}</h1>
57      <div dangerouslySetInnerHTML={{ __html: post.content }} />
58    </article>
59  );
60}

Base metadata and inheritance

In Next.js, metadata can be defined at multiple levels, with an inheritance system from more general to more specific:

1// app/layout.tsx - Meta tags for the entire application
2export const metadata: Metadata = {
3  metadataBase: new URL('https://acme.com'),
4  title: {
5    template: '%s | Home Page',
6    default: 'Home Page', // when there is no title on the child page
7  },
8  description: 'Default description for all pages',
9  //
10};
11
12// app/blog/layout.tsx - Meta tags for the blog section
13export const metadata: Metadata = {
14  title: {
15    template: '%s | Blog',
16    default: 'Blog', 
17  },
18  description: 'Blog with the latest industry news',
19  //
20};
21
22// app/blog/[slug]/page.tsx - Meta tags for a specific post
23export async function generateMetadata({ params }) {
24  const post = await getBlogPost((await params).slug);
25  return {
26    title: post.title, // will be used in the template: "Post Title | Blog"
27    description: post.excerpt,
28    //
29  };
30}

Implementing advanced SEO features

Canonical tags

Canonical tags are important for preventing content duplication, which can negatively affect SEO:

1// app/products/[id]/page.tsx
2export async function generateMetadata({ params }) {
3  const product = await getProduct((await params).id);
4  
5  return {
6    title: product.name,
7    description: product.description,
8    alternates: {
9      canonical: `https://twojsklep.pl/products/${product.id}`,
10    },
11  };
12}

Multi-language support with hreflang

For multilingual websites, hreflang tags are crucial for directing users to the appropriate language version:

1// app/[lang]/layout.tsx
2export async function generateMetadata({ params }) {
3  const { lang } = await params;
4  
5  return {
6    alternates: {
7      languages: {
8        'pl-PL': `https://twojaspage.pl/pl`,
9        'en-US': `https://twojaspage.pl/en`,
10        'de-DE': `https://twojaspage.pl/de`,
11      },
12    },
13  };
14}

Structured data (JSON-LD)

Structured data helps search engines better understand page content and can lead to enhanced search results:

1// components/ProductJsonLd.tsx
2export default async function ProductJsonLd({ product }) {
3  return (
4    <script
5      type="application/ld+json"
6      dangerouslySetInnerHTML={{
7        __html: JSON.stringify({
8          '@context': 'https://schema.org',
9          '@type': 'Product',
10          name: product.name,
11          description: product.description,
12          image: product.images[0],
13          offers: {
14            '@type': 'Offer',
15            price: product.price,
16            priceCurrency: 'PLN',
17            availability: product.inStock
18              ? 'https://schema.org/InStock'
19              : 'https://schema.org/OutOfStock',
20          },
21          brand: {
22            '@type': 'Brand',
23            name: product.brand,
24          },
25          // Other product-specific data...
26        }),
27      }}
28    />
29  );
30}
31
32// Used in the page component:
33// app/products/[id]/page.tsx
34import ProductJsonLd from '@/components/ProductJsonLd';
35
36export default async function ProductPage({ params }) {
37  const product = await getProduct((await params).id);
38  
39  return (
40    <>
41      <ProductJsonLd product={product} />
42      <div>{/* product UI */}</div>
43    </>
44  );
45}

Optimization for search engine robots

The robots.txt file

Next.js 13+ offers a simple way to configure robots.txt by creating a static or dynamic file:

1// app/robots.ts (or .js/.tsx/.jsx)
2import { MetadataRoute } from 'next';
3
4export default function robots(): MetadataRoute.Robots {
5  return {
6    rules: {
7      userAgent: '*',
8      allow: '/',
9      disallow: ['/admin/', '/private/', '/api/'],
10    },
11    sitemap: 'https://twojaspage.pl/sitemap.xml',
12  };
13}

Dynamic robots.txt configuration

In production vs. development scenarios, we often need different robots configurations:

1// app/robots.ts
2import { MetadataRoute } from 'next';
3
4export default function robots(): MetadataRoute.Robots {
5  const baseUrl = process.env.NEXT_PUBLIC_BASE_URL || 'http://localhost:3000';
6  
7  return {
8    rules: {
9      userAgent: '*',
10      allow: '/',
11      disallow: process.env.NODE_ENV === 'production' 
12        ? ['/admin/'] 
13        : ['/', '/api/', '/admin/'], // Block indexing in the development environment
14    },
15    sitemap: `${baseUrl}/sitemap.xml`,
16  };
17}

SEO testing and validation

Testing tools

Regular SEO testing is key to maintaining and improving search engine visibility:

  1. Google Search Console - official tool for monitoring presence in Google
  2. Lighthouse - performance and SEO audit
  3. Screaming Frog - crawling and technical SEO audit
  4. SEMrush/Ahrefs - comprehensive SEO analytics tools

SEO test automation

Add SEO tests to your CI/CD flow:

1// scripts/seo-tests.mjs
2import lighthouse from 'lighthouse';
3import puppeteer from 'puppeteer';
4import { writeFileSync } from 'fs';
5
6async function runLighthouseTests() {
7  const browser = await puppeteer.launch({ headless: true });
8  const pages = [
9    '/',
10    '/blog',
11    '/products',
12    // Add key pages...
13  ];
14
15  for (const page of pages) {
16    const url = `https://twojadomena.pl${page}`;
17    
18    const { lhr } = await lighthouse(url, {
19      port: (new URL(browser.wsEndpoint())).port,
20      output: 'html',
21      logLevel: 'info',
22      onlyCategories: ['seo', 'best-practices', 'accessibility'],
23    });
24    
25    console.log(`SEO score for ${page}: ${lhr.categories.seo.score * 100}`);
26    writeFileSync(`./lighthouse-seo-${page.replace(/\//g, '-')}.html`, lhr.report);
27    
28    // You can add assertions and notifications if the results fall below expectations
29    if (lhr.categories.seo.score < 0.9) {
30      console.error(`⚠️ SEO score for ${page} is too low: ${lhr.categories.seo.score * 100}%`);
31      // Can send a notification or abort build...
32    }
33  }
34  
35  await browser.close();
36}
37
38runLighthouseTests().catch(console.error);

SEO best practices for Next.js

  1. Use SSG/ISR for pages that don't need 100% current data - this ensures faster loading
  2. Implement dynamic metadata for each page
  3. Optimize loading of critical resources (CSS/JS) to improve Core Web Vitals
  4. Use semantic HTML5 for better accessibility and search engine understanding
  5. Ensure responsiveness on all devices
  6. Optimize images using the component
    next/image
  7. Apply structured data appropriate for your content type
  8. Create SEO-friendly URLs - short, descriptive, containing keywords

Example of complete SEO integration

1// lib/seo-utils.ts
2export function generateOpenGraph(pageData) {
3  const baseUrl = process.env.NEXT_PUBLIC_BASE_URL;
4  const image = pageData.image || '/images/default-share.jpg';
5  
6  return {
7    type: pageData.type || 'website',
8    title: pageData.title,
9    description: pageData.description,
10    siteName: 'Your Site Name',
11    url: `${baseUrl}${pageData.url || ''}`,
12    images: [
13      {
14        url: image.startsWith('http') ? image : `${baseUrl}${image}`,
15        width: 1200,
16        height: 630,
17        alt: pageData.title,
18      },
19    ],
20    locale: 'pl_PL',
21  };
22}
23
24// Example of usage in a page:
25// app/kursy/[slug]/page.tsx
26import { generateOpenGraph } from '@/lib/seo-utils';
27
28export async function generateMetadata({ params }) {
29  const course = await getCourse((await params).slug);
30  
31  if (!course) {
32    return {
33      title: 'Course not found',
34      description: 'Sorry, we didn't find this course',
35    };
36  }
37  
38  const pageData = {
39    title: course.title,
40    description: course.shortDescription,
41    image: course.coverImage,
42    url: `/kursy/${(await params).slug}`,
43    type: 'article',
44  };
45  
46  return {
47    title: course.title,
48    description: course.shortDescription,
49    openGraph: generateOpenGraph(pageData),
50    twitter: {
51      card: 'summary_large_image',
52      title: course.title,
53      description: course.shortDescription,
54      images: [course.coverImage],
55    },
56    alternates: {
57      canonical: `https://twojadomena.pl/kursy/${(await params).slug}`,
58    },
59  };
60}

Summary

Effective SEO and metadata management in Next.js requires:

  1. Implementing appropriate static and dynamic metadata
  2. Using the new Metadata API in the App Router
  3. Adding structured data (JSON-LD)
  4. Configuring robots.txt and the sitemap
  5. Regular testing and monitoring of SEO results

Thanks to Next.js built-in features and a well-thought-out SEO strategy, you can significantly improve your application's visibility in search engines, which translates into more organic traffic and better conversions.

Remember that SEO is an ongoing process - regular monitoring, testing, and optimization are necessary to maintain and improve positions in search results.

Go to CodeWorlds