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.
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.
Metadata is a critical element of every SEO strategy, providing search engines with context and information about the page content. Key metadata includes:
Next.js 13 introduced a new, more intuitive API for managing metadata through files
layout.js or page.js in the App Router.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}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}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}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}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 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}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}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}Regular SEO testing is key to maintaining and improving search engine visibility:
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);next/image1// 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}Effective SEO and metadata management in Next.js requires:
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.