SEO (Search Engine Optimization) is one of the key factors to rank your website higher on Google and receive more traffic.
In this tutorial, I will show you what I have done to optimize this blog for SEO by providing a checklist that you can follow easily.
I have spent a lot of time researching and learning about SEO, especially for Next.js, and found some optimal ways to do it. So I hope this tutorial will help you save time and effort.
I will put the items covered in this tutorial inside the Table of Contents below. You can click on any item to jump to the section.
Please note that some parts are different for Next.js Pages Router and Next.js App Router, I will mention it in the tutorial. Otherwise, it can be applied to both.
Meta tags provide information about your website to search engines and social media platforms.
Your website should include the following standard meta tags:
title
description
keywords
robots
viewport
charSet
Open Graph meta tags:
og:site_name
og:locale
og:title
og:description
og:type
og:url
og:image
og:image:alt
og:image:type
og:image:width
og:image:height
Article meta tags (actually it's also OpenGraph):
article:published_time
article:modified_time
article:author
Twitter meta tags:
twitter:card
twitter:site
twitter:creator
twitter:title
twitter:description
twitter:image
Please note that for the og:image
and twitter:image
tags, you should use the PNG or JPG format as some social media platforms will not read the WebP format.
For example, the current page has the following meta tags when I use Next.js Pages Router:
pages/[slug].tsx import Head from " next/head " ;
export default function Page () {
return (
< Head >
< title >
Next.js SEO: The Complete Checklist to Boost Your Site Ranking
</ title >
< meta
name = " description "
content = " Learn how to optimize your Next.js website for SEO by following this complete checklist. "
/>
< meta
name = " keywords "
content = " nextjs seo complete checklist, nextjs seo tutorial "
/>
< meta name = " robots " content = " index, follow " />
< meta name = " googlebot " content = " index, follow " />
< meta name = " viewport " content = " width=device-width, initial-scale=1.0 " />
< meta charSet = " utf-8 " />
< meta property = " og:site_name " content = " Blog | Minh Vu " />
< meta property = " og:locale " content = " en_US " />
< meta
property = " og:title "
content = " Next.js SEO: The Complete Checklist to Boost Your Site Ranking "
/>
< meta
property = " og:description "
content = " Learn how to optimize your Next.js website for SEO by following this complete checklist. "
/>
< meta property = " og:type " content = " website " />
< meta property = " og:url " content = " https://dminhvu.com/nextjs-seo " />
< meta
property = " og:image "
content = " https://ik.imagekit.io/dminhvu/assets/nextjs-seo/thumbnail.png?tr=f-png "
/>
< meta property = " og:image:alt " content = " Next.js SEO " />
< meta property = " og:image:type " content = " image/png " />
< meta property = " og:image:width " content = " 1200 " />
< meta property = " og:image:height " content = " 630 " />
< meta
property = " article:published_time "
content = " 2024-01-11T11:35:00+07:00 "
/>
< meta
property = " article:modified_time "
content = " 2024-01-11T11:35:00+07:00 "
/>
< meta
property = " article:author "
content = " https://www.linkedin.com/in/dminhvu02 "
/>
< meta name = " twitter:card " content = " summary_large_image " />
< meta name = " twitter:site " content = " @dminhvu02 " />
< meta name = " twitter:creator " content = " @dminhvu02 " />
< meta
name = " twitter:title "
content = " Next.js SEO: The Complete Checklist to Boost Your Site Ranking "
/>
< meta
name = " twitter:description "
content = " Learn how to optimize your Next.js website for SEO by following this complete checklist. "
/>
< meta
name = " twitter:image "
content = " https://ik.imagekit.io/dminhvu/assets/nextjs-seo/thumbnail.png?tr=f-png "
/>
</ Head >
) ;
}
For Next.js App Router, it's even easier to define those tags by using the built-in export const metadata
argument for static metadata, and the generateMetadata
function for pages that have dynamic data (blog posts, products, etc.).
Let's find out how to do it. You can visit the metadata docs to learn more.
You can define static metadata by adding the export const metadata
argument inside page.tsx
or layout.tsx
:
app/layout.tsx import type { Viewport , Metadata } from " next " ;
export const viewport : Viewport = {
width : " device-width " ,
initialScale : 1 ,
themeColor : " #ffffff "
} ;
export const metadata : Metadata = {
metadataBase : new URL ( " https://dminhvu.com " ) ,
openGraph : {
siteName : " Blog | Minh Vu " ,
type : " website " ,
locale : " en_US "
} ,
robots : {
index : true ,
follow : true ,
" max-image-preview " : " large " ,
" max-snippet " : - 1 ,
" max-video-preview " : - 1 ,
googleBot : " index, follow "
} ,
alternates : {
types : {
" application/rss+xml " : " https://dminhvu.com/rss.xml "
}
} ,
applicationName : " Blog | Minh Vu " ,
appleWebApp : {
title : " Blog | Minh Vu " ,
statusBarStyle : " default " ,
capable : true
} ,
verification : {
google : " YOUR_DATA " ,
yandex : [ " YOUR_DATA " ],
other : {
" msvalidate.01 " : [ " YOUR_DATA " ],
" facebook-domain-verification " : [ " YOUR_DATA " ]
}
} ,
icons : {
icon : [
{
url : " /favicon.ico " ,
type : " image/x-icon "
} ,
{
url : " /favicon-16x16.png " ,
sizes : " 16x16 " ,
type : " image/png "
}
// add favicon-32x32.png, favicon-96x96.png, android-chrome-192x192.png
],
shortcut : [
{
url : " /favicon.ico " ,
type : " image/x-icon "
}
],
apple : [
{
url : " /apple-icon-57x57.png " ,
sizes : " 57x57 " ,
type : " image/png "
} ,
{
url : " /apple-icon-60x60.png " ,
sizes : " 60x60 " ,
type : " image/png "
}
// add apple-icon-72x72.png, apple-icon-76x76.png, apple-icon-114x114.png, apple-icon-120x120.png, apple-icon-144x144.png, apple-icon-152x152.png, apple-icon-180x180.png
]
}
} ;
app/page.tsx import { Metadata } from " next " ;
export const metadata : Metadata = {
title : " Elastic Stack, Next.js, Python, JavaScript Tutorials | dminhvu " ,
description :
" dminhvu.com - Programming blog for everyone to learn Elastic Stack, Next.js, Python, JavaScript, React, Machine Learning, Data Science, and more. " ,
keywords : [
" elastic " ,
" python " ,
" javascript " ,
" react " ,
" machine learning " ,
" data science "
],
openGraph : {
url : " https://dminhvu.com " ,
type : " website " ,
title : " Elastic Stack, Next.js, Python, JavaScript Tutorials | dminhvu " ,
description :
" dminhvu.com - Programming blog for everyone to learn Elastic Stack, Next.js, Python, JavaScript, React, Machine Learning, Data Science, and more. " ,
images : [
{
url : " https://dminhvu.com/images/home/thumbnail.png " ,
width : 1200 ,
height : 630 ,
alt : " dminhvu "
}
]
} ,
twitter : {
card : " summary_large_image " ,
title : " Elastic Stack, Next.js, Python, JavaScript Tutorials | dminhvu " ,
description :
" dminhvu.com - Programming blog for everyone to learn Elastic Stack, Next.js, Python, JavaScript, React, Machine Learning, Data Science, and more. " ,
creator : " @dminhvu02 " ,
site : " @dminhvu02 " ,
images : [
{
url : " https://dminhvu.com/images/home/thumbnail.png " ,
width : 1200 ,
height : 630 ,
alt : " dminhvu "
}
]
} ,
alternates : {
canonical : " https://dminhvu.com "
}
} ;
Dynamic metadata can be defined by using the generateMetadata
function, this is useful when you have dynamic pages like [slug]/page.tsx
, or [id]/page.tsx
:
app/[slug]/page.tsx import type { Metadata , ResolvingMetadata } from " next " ;
type Params = {
slug : string ;
} ;
type Props = {
params : Params ;
searchParams : { [ key : string ]: string | string [] | undefined } ;
} ;
export async function generateMetadata (
{ params , searchParams }: Props ,
parent : ResolvingMetadata
) : Promise < Metadata > {
const { slug } = params ;
const post : Post = await fetch ( ` YOUR_ENDPOINT ` , {
method : " GET " ,
next : {
revalidate : 60 * 60 * 24
}
}) . then (( res ) => res . json ()) ;
return {
title : `${ post . title } | dminhvu ` ,
authors : [
{
name : post . author || " Minh Vu "
}
],
description : post . description ,
keywords : post . keywords ,
openGraph : {
title : `${ post . title } | dminhvu ` ,
description : post . description ,
type : " article " ,
url : ` https://dminhvu.com/ ${ post . slug }` ,
publishedTime : post . created_at ,
modifiedTime : post . modified_at ,
authors : [ " https://dminhvu.com/about " ],
tags : post . categories ,
images : [
{
url : ` https://ik.imagekit.io/dminhvu/assets/ ${ post . slug } /thumbnail.png?tr=f-png ` ,
width : 1024 ,
height : 576 ,
alt : post . title ,
type : " image/png "
}
]
} ,
twitter : {
card : " summary_large_image " ,
site : " @dminhvu02 " ,
creator : " @dminhvu02 " ,
title : `${ post . title } | dminhvu ` ,
description : post . description ,
images : [
{
url : ` https://ik.imagekit.io/dminhvu/assets/ ${ post . slug } /thumbnail.png?tr=f-png ` ,
width : 1024 ,
height : 576 ,
alt : post . title
}
]
} ,
alternates : {
canonical : ` https://dminhvu.com/ ${ post . slug }`
}
} ;
}
Please note that the charSet
and viewport
are automatically added by Next.js App Router, so you don't need to define them.
This part can be applied to both Pages Router and App Router.
I have written an in-depth post about How to Add JSON-LD Schema to Your Next.js Website that you can read to dive deeper into this topic.
JSON-LD is a lightweight Linked Data format. It is easy for machines to parse and generate. It is currently one of the most widely used formats for Linked Data.
You can easily generate JSON-LD Schema for your website by using the Schema Markup Generator Tool .
For example, the current page has the following JSON-LD Schema:
const jsonLd = {
" @context " : " https://schema.org " ,
" @type " : " BlogPosting " ,
mainEntityOfPage : {
" @type " : " WebPage " ,
" @id " : " https://dminhvu.com/nextjs-seo "
} ,
headline : " Next.js SEO: The Complete Checklist to Boost Your Site Ranking " ,
description :
" Learn how to optimize your Next.js website for SEO by following this complete checklist. " ,
image :
" https://ik.imagekit.io/dminhvu/assets/nextjs-seo/thumbnail.png?tr=f-png " ,
dateCreated : " 2024-01-11T11:35:00+07:00 " ,
datePublished : " 2024-01-11T11:35:00+07:00 " ,
dateModified : " 2024-01-11T11:35:00+07:00 " ,
author : {
" @type " : " Person " ,
name : " Minh Vu " ,
url : " https://www.linkedin.com/in/dminhvu02 "
} ,
publisher : {
" @type " : " Person " ,
name : " Minh Vu " ,
logo : {
" @type " : " ImageObject " ,
url : " https://dminhvu.com/avatar_zoom.jpg "
}
} ,
inLanguage : " en-US " ,
isFamilyFriendly : " true "
} ;
And I can put it anywhere, whether inside the head
tag or the body
tag is fine:
import Head from " next/head " ;
export default function Page () {
return (
< div >
{ /* other parts */ }
< script
type = " application/ld+json "
dangerouslySetInnerHTML ={ { __html : JSON . stringify ( jsonLd ) } }
/>
</ div >
) ;
}
Your website should provide a sitemap so that search engines can easily crawl and index your pages.
For Next.js Pages Router, you can use next-sitemap to generate a sitemap for your Next.js website after building.
For example, running the following command will install next-sitemap
and generate a sitemap for this blog:
npm install next-sitemap
npx next-sitemap
A sitemap will be generated at public/sitemap.xml
:
public/sitemap.xml < urlset xmlns = " http://www.sitemaps.org/schemas/sitemap/0.9 " xmlns : news = " http://www.google.com/schemas/sitemap-news/0.9 " xmlns : xhtml = " http://www.w3.org/1999/xhtml " xmlns : mobile = " http://www.google.com/schemas/sitemap-mobile/1.0 " xmlns : image = " http://www.google.com/schemas/sitemap-image/1.1 " xmlns : video = " http://www.google.com/schemas/sitemap-video/1.1 " >
< url >
< loc > https://dminhvu.com </ loc >
< lastmod > 2024-01-11T02:03:09.613Z </ lastmod >
< changefreq > daily </ changefreq >
< priority > 0.7 </ priority >
</ url >
<!-- other pages -->
</ urlset >
Please visit the next-sitemap page for more information.
For Next.js App Router, you can define the sitemap.ts
file at app/sitemap.ts
:
app/sitemap.ts import {
getAllCategories ,
getAllPostSlugsWithModifyTime
} from " @/utils/getData " ;
import { MetadataRoute } from " next " ;
export default async function sitemap () : Promise < MetadataRoute . Sitemap > {
const defaultPages = [
{
url : " https://dminhvu.com " ,
lastModified : new Date () ,
changeFrequency : " daily " ,
priority : 1
} ,
{
url : " https://dminhvu.com/about " ,
lastModified : new Date () ,
changeFrequency : " monthly " ,
priority : 0.9
} ,
{
url : " https://dminhvu.com/contact " ,
lastModified : new Date () ,
changeFrequency : " monthly " ,
priority : 0.9
}
// other pages
];
const postSlugs = await getAllPostSlugsWithModifyTime () ;
const categorySlugs = await getAllCategories () ;
const sitemap = [
... defaultPages ,
... postSlugs . map (( e : any ) => ({
url : ` https://dminhvu.com/ ${ e . slug }` ,
lastModified : e . modified_at ,
changeFrequency : " daily " ,
priority : 0.8
})) ,
... categorySlugs . map (( e : any ) => ({
url : ` https://dminhvu.com/category/ ${ e }` ,
lastModified : new Date () ,
changeFrequency : " daily " ,
priority : 0.7
}))
];
return sitemap ;
}
With this sitemap.ts
file created, you can access the sitemap at https://dminhvu.com/sitemap.xml
.
< urlset xmlns = " http://www.sitemaps.org/schemas/sitemap/0.9 " >
< url >
< loc > https://dminhvu.com </ loc >
< lastmod > 2024-01-11T02:03:09.613Z </ lastmod >
< changefreq > daily </ changefreq >
< priority > 0.7 </ priority >
</ url >
<!-- other pages -->
</ urlset >
Visit Next.js App Router Sitemap to learn more.
A robots.txt
file should be added to tell search engines which pages to crawl and which pages to ignore.
For Next.js Pages Router, you can create a robots.txt
file at public/robots.txt
:
public/robots.txt User-agent: *
Disallow:
Sitemap: https://dminhvu.com/sitemap.xml
You can prevent the search engine from crawling a page (usually search result pages, noindex pages, etc.) by adding the following line:
public/robots.txt User-agent: *
Disallow: /search?q=
Disallow: /admin
For Next.js App Router, you don't need to manually define a robots.txt
file. Instead, you can define the robots.ts
file at app/robots.ts
:
app/robots.ts import { MetadataRoute } from " next " ;
export default function robots () : MetadataRoute . Robots {
return {
rules : {
userAgent : " * " ,
allow : [ " / " ],
disallow : [ " /search?q= " , " /admin/ " ]
} ,
sitemap : [ " https://dminhvu.com/sitemap.xml " ]
} ;
}
With this robots.ts
file created, you can access the robots.txt
file at https://dminhvu.com/robots.txt
.
User-agent: *
Allow: /
Disallow: /search?q=
Disallow: /admin
Sitemap: https://dminhvu.com/sitemap.xml
There are some important link
tags like the meta
tags that you should include on your website:
canonical
alternate
icon
apple-touch-icon
manifest
For example, the current page has the following link
tags if I use the Pages Router:
pages/_app.tsx import Head from " next/head " ;
export default function Page () {
return (
< Head >
{ /* other parts */ }
< link
rel = " alternate "
type = " application/rss+xml "
href = " https://dminhvu.com/rss.xml "
/>
< link rel = " icon " href = " /favicon.ico " type = " image/x-icon " />
< link rel = " apple-touch-icon " sizes = " 57x57 " href = " /apple-icon-57x57.png " />
< link rel = " apple-touch-icon " sizes = " 60x60 " href = " /apple-icon-60x60.png " />
{ /* add apple-touch-icon-72x72.png, apple-touch-icon-76x76.png, apple-touch-icon-114x114.png, apple-touch-icon-120x120.png, apple-touch-icon-144x144.png, apple-touch-icon-152x152.png, apple-touch-icon-180x180.png */ }
< link
rel = " icon "
type = " image/png "
href = " /favicon-16x16.png "
sizes = " 16x16 "
/>
{ /* add favicon-32x32.png, favicon-96x96.png, android-chrome-192x192.png */ }
</ Head >
) ;
}
pages/[slug].tsx import Head from " next/head " ;
export default function Page () {
return (
< Head >
{ /* other parts */ }
< link rel = " canonical " href = " https://dminhvu.com/nextjs-seo " />
</ Head >
) ;
}
For Next.js App Router, the link tags can be defined using the export const metadata
or generateMetadata
similar to the meta tags section.
The code below is exactly the same as the meta tags for Next.js App Router section above.
app/layout.tsx export const metadata : Metadata = {
// other parts
alternates : {
types : {
" application/rss+xml " : " https://dminhvu.com/rss.xml "
}
} ,
icons : {
icon : [
{
url : " /favicon.ico " ,
type : " image/x-icon "
} ,
{
url : " /favicon-16x16.png " ,
sizes : " 16x16 " ,
type : " image/png "
}
// add favicon-32x32.png, favicon-96x96.png, android-chrome-192x192.png
],
shortcut : [
{
url : " /favicon.ico " ,
type : " image/x-icon "
}
],
apple : [
{
url : " /apple-icon-57x57.png " ,
sizes : " 57x57 " ,
type : " image/png "
} ,
{
url : " /apple-icon-60x60.png " ,
sizes : " 60x60 " ,
type : " image/png "
}
// add apple-icon-72x72.png, apple-icon-76x76.png, apple-icon-114x114.png, apple-icon-120x120.png, apple-icon-144x144.png, apple-icon-152x152.png, apple-icon-180x180.png
]
}
} ;
app/page.tsx export const metadata : Metadata = {
// other parts
alternates : {
canonical : " https://dminhvu.com "
}
} ;
Next.js provides a built-in component called <Script>
to add external scripts to your website.
For example, you can add Google Analytics to your website by adding the following script tag:
pages/_app.tsx import Head from " next/head " ;
import Script from " next/script " ;
export default function Page () {
return (
< Head >
{ /* other parts */ }
{ process . env . NODE_ENV === " production " && (
<>
< Script async strategy = " afterInteractive " id = " analytics " >
{ `
window.dataLayer = window.dataLayer || [];
function gtag(){dataLayer.push(arguments);}
gtag('js', new Date());
gtag('config', 'G-XXXXXXXXXX');
` }
</ Script >
</>
) }
</ Head >
) ;
}
Next.js App Router introduces a new library called @next/third-parties for:
To use the @next/third-parties
library, you need to install it:
npm install @next/third-parties
Then, you can add the following code to your app/layout.tsx
:
app/layout.tsx import { GoogleTagManager } from " @next/third-parties/google " ;
import { GoogleAnalytics } from " @next/third-parties/google " ;
import Head from " next/head " ;
export default function Page () {
return (
< html lang = " en " className = " scroll-smooth " suppressHydrationWarning >
{ process . env . NODE_ENV === " production " && (
<>
< GoogleAnalytics gaId = " G-XXXXXXXXXX " />
{ /* other scripts */ }
</>
) }
{ /* other parts */ }
</ html >
) ;
}
Please note that you don't need to include both GoogleTagManager and GoogleAnalytics if you only use one of them.
This part can be applied to both Pages Router and App Router.
Image optimization is also an important part of SEO as it helps your website load faster.
Faster image rendering speed will contribute to the Google PageSpeed score, which can improve user experience and SEO.
You can use next/image to optimize images in your Next.js website.
For example, the following code will optimize this post thumbnail:
import Image from " next/image " ;
export default function Page () {
return (
< Image
src = " https://ik.imagekit.io/dminhvu/assets/nextjs-seo/thumbnail.png?tr=f-webp "
alt = " Next.js SEO "
width ={ 1200 }
height ={ 630 }
/>
) ;
}
Remember to use a CDN to serve your media (images, videos, etc.) to improve the loading speed. Here I used ImageKit to serve my images. Some people prefer Cloudinary or Vercel built-in image optimization.
You can also use DigitalOcean Spaces to store your media files. Use this link to get $200 free credit for 60 days to try it out.
For the image format, I prefer to use WebP because it has a smaller size than PNG and JPEG .
I hope this tutorial will help you optimize your Next.js website for SEO.
In general, there are several important points that you should pay attention to:
Meta tags
JSON-LD Schema
Sitemap
robots.txt
Link tags: use the <link>
tag to define canonical URLs, icons, etc.
Script optimization
Use the Next.js's <Script>
component
Use @next/third-parties
for common third-party scripts
Image optimization
Use the Next.js's <Image>
component
Use a CDN to cache images for better speed
Use modern formats such as WebP
If you have any questions, please leave a comment below.
Comments
Minh Vu
Feb 22, 2024
I just added the robots.txt section for Next.js App Router. Cheers!
kiwi
Feb 25, 2024
Thanks for the in-depth tutorial, can you elaborate more about JSON LD?
jayden
Feb 28, 2024
thorough and detailed explanation
Rowland Ricketts
Mar 05, 2024
Thanks for sharing this,
kunkka
May 27, 2024
Thanks for sharing, good article
David
Jul 14, 2024
Indeed, this tutorial will really save me a lot of time and effort. Cheers, man!
David
Jul 14, 2024
Indeed, this tutorial will really save me a lot of time and effort. Cheers, man!
Enes
Aug 20, 2024
This is very concise, thank you very much!
Enes
Aug 20, 2024
This is very concise, thank you very much!