Drum Roll...
First off, no need doing the ReactJS vs NextJS banter. If you're thinking about optimizing your website/web app for:
- Search Engines - Crawling and indexing - so your site and content can rank on page search
- Building dynamic metadata so you can get that fancy link description and image preview when you share your links ...then you already know that NextJS is your best bet.
Let's not even get started about Pages Router vs App Router, I personally love the amazing features that come with using the App Router structure. From easy file-based routing, more flexible routing patterns for dynamic segments and catch-all routes to server actions and all that. So, this guide will only be treating how to implement SEO with the App Router.
Getting Started
First, let's bootstrap a new NextJS project, if you haven't already. Follow the prompts and set your preference (I don't skip accepting Typescript - so should you π)
npx create-next-app@latest
Open the code editor and start the development server.
npm run dev
Metadata
import type { Metadata } from "next";
import { Inter } from "next/font/google";
import "./globals.css";
const inter = Inter({ subsets: ["latin"] });
export const metadata: Metadata = {
title: "Create Next App",
description: "Generated by create next app",
};
export default function RootLayout({
children,
}: Readonly<{
children: React.ReactNode;
}>) {
return (
<html lang="en">
<body className={inter.className}>{children}</body>
</html>
);
}
The code snippet above shows the default RootLayout component found in the layout.tsx
file which serves as the global layout which all pages and sub-pages will inherit any UI from - this also means, the metadata object you're exporting from this component will be shared by all other pages unless you explicitly export a metadata object from individual pages (page.tsx
or page.jsx
).
- You can define and export a variable named
metadata
using Metadata type (for static details) or a function namedgenerateMetadata
(this function is especially helpful for loading dynamic data which can then be used to create metadata object for dynamic pages). - Metadata can be defined in layout files and overridden in nested pages.
- NextJS automatically generates appropriate tags for each page based on your metadata.
What does a full-blown Metadata object look like?
export const metadata = {
title: 'My Awesome Website',
description: 'Discover amazing content and services on My Awesome Website',
// Basic metadata
applicationName: 'My Awesome App',
authors: [{ name: 'Stephen Omoregie', url: 'https://cre8stevedev.me' }],
generator: 'Next.js',
keywords: ['next.js', 'react', 'javascript'],
referrer: 'origin-when-cross-origin',
themeColor: '#4285f4',
colorScheme: 'dark',
viewport: 'width=device-width, initial-scale=1',
creator: 'Stephen Omoregie',
publisher: 'Cre8steve Dev',
// Open Graph metadata
openGraph: {
title: 'My Awesome Website',
description: 'Discover amazing content and services',
url: 'https://cre8stevedev.me',
siteName: 'My Awesome Website',
images: [
{
url: 'https://myawesomewebsite.com/og-image.jpg',
width: 1200, // This is the recommended size in pixels
height: 630,
alt: 'My Awesome Website og-image',
},
],
locale: 'en_US',
type: 'website',
},
// Twitter metadata
twitter: {
card: 'summary_large_image',
title: 'My Awesome Website',
description: 'Discover amazing content and services',
creator: '@cre8stevedev',
images: ['https://myawesomewebsite.com/twitter-image.jpg'],
},
// Verification for search engines
// You can get these values from the respective
// search engines when you submit your site for
// indexing
verification: {
google: 'google-site-verification=1234567890',
yandex: 'yandex-verification=1234567890',
yahoo: 'yahoo-site-verification=1234567890',
},
// Alternate languages
alternates: {
canonical: 'https://myawesomewebsite.com',
languages: {
'en-US': 'https://myawesomewebsite.com/en-US',
'es-ES': 'https://myawesomewebsite.com/es-ES',
},
},
// Icons
icons: {
icon: '/favicon.ico',
shortcut: '/favicon-16x16.png',
apple: '/apple-touch-icon.png',
other: [
{
rel: 'apple-touch-icon-precomposed',
url: '/apple-touch-icon-precomposed.png',
},
],
},
// Manifest
manifest: '/site.webmanifest',
// App-specific metadata
appleWebApp: {
capable: true,
title: 'My Awesome App',
statusBarStyle: 'black-translucent',
},
// Robots directives
robots: {
index: true,
follow: true,
nocache: true,
googleBot: {
index: true,
follow: true,
noimageindex: true,
'max-video-preview': -1,
'max-image-preview': 'large',
'max-snippet': -1,
},
},
// Format detection
formatDetection: {
email: false,
address: false,
telephone: false,
},
};
Don't run, don't run! No, you don't have to use all these properties for your page. They're just to serve as a reference for creating your own metadata object that meets your need. It's safe to say many of the properties are quite self-explanatory. The OpenGraph object uses the OpenGraph protocol that allows your app have link-image-description previews for social sharing. There's also an object property for twitter-specific metatada.
Let's Dive Right In - How To use It In your Project
Remember, if you have static metadata that you want nested pages to share, just place the metadata object in the top-most component (in this case that would be the layout.tsx
file in the route structure. example.
my-nextjs-project/
β
βββ app/
β βββ (auth)/
β β βββ signin/
β β β βββ page.tsx
β β βββ signup/
β β β βββ page.tsx
β β βββ layout.tsx
β β
β βββ about/
β β βββ page.tsx
β βββ api/
β β βββ post/
β β β βββ [slug]/
β β βββ route.ts
β βββ layout.tsx
β βββ robots.ts
β βββ sitemap.ts
β βββ page.tsx
βββ public/
β βββ favicon.ico
β βββ ...
β
βββ components/
β βββ ...
β
βββ lib/
β βββ customMetaDataGenerator.ts
β
βββ styles/
β βββ globals.css
β
βββ package.json
βββ next.config.js
βββ tsconfig.json
βββ README.md
Take note of the project structure above, we'll be making reference to it to understand the placement of your files/configurations.
Creating a Reusable Metadata Function
Let's create a utility function that will take an object of properties which we can pass customized values to generate metadata on any page. We'll call this custom function and assign it to the 'metadata' variable object which will be exported from the page.
// File location: @/lib/customMetaDataGenerator.ts
import { Metadata } from 'next';
interface PageSEOProps {
title: string;
description?: string;
canonicalUrl?: string;
ogType?: string;
ogImage?: string;
twitterCard?: string;
keywords?: string[];
}
export function customMetaDataGenerator({
title,
description = "Join the vibrant community that's bringing Nigerians together like never before...",
canonicalUrl = 'https://naijarium.vercel.app',
ogType = 'website',
keywords = [
"an array", "of default", "keywords"
],
ogImage = 'https://url-to-your-image-this-is-a-default-value-for-optional-parameter',
twitterCard = 'summary_large_image',
}: PageSEOProps): Metadata {
// Create Site Title
const siteTitle = 'Your Website Name';
const fullTitle = `${title} | ${siteTitle}`;
return {
title: fullTitle,
description,
keywords: keywords.join(', '),
openGraph: {
title: fullTitle,
description,
type: ogType,
url: canonicalUrl,
images: [
{
url: ogImage,
},
],
},
twitter: {
card: twitterCard,
title: fullTitle,
description,
images: [ogImage],
},
alternates: {
canonical: canonicalUrl,
},
};
}
Using the custom function in your root layout
// @/app/layout.tsx
import { customMetaDataGenerator } from '@/lib/customMetaDataGenerator';
// Define Metadata for the general site layout
// We're relying on the default parameters defined in the function,
// That's why we're only passing `title` in the object
export const metadata: Metadata = customMetaDataGenerator({
title: 'Social Media Forum for Nigerians',
});
...
// The rest of your layout.tsx code follows.
You can equally do this in all your page.tsx for the individual routes - This is helpful if you have static data - For instance a multi-page website with static data. But what if you're implementing this for dynamic websites - portfolio with different projects, blog, e-commerce, etc. Then you need to be able to generate data specific to the requested resource and build the metadata on the server.
Generating Dynamic Metadata
Note, the idea of having metadata that search engines can use, is that it has to run on the server. So, if your page.tsx
is a client component with the 'use client' directive, then the next best thing is to place the metadata generation function in the layout.tsx
for that route segment, that generate the metadata on the server and return your client component.
When you export a function named generateMetadata
in your page.tsx
or layout.tsx
, for instance, a dynamic route that takes a slug
as a params; this function will be automatically called by NextJS on the server and will also have access to the parameter(s) for that route. So, you can use the slug to fetch data for the dynamic resource and build your metadata object that will be returned for that page.
Here's an example of creating dynamic metadata for a url:
https://naijarium.vercel.app/post/how-to-become-a-full-stack-dev
// @/app/post/[slug]/layout.tsx
import { customMetaDataGenerator } from '@/lib/customMetaDataGenerator';
import { Metadata } from 'next';
import { fetchSinglePost } from '@/lib/fetchSinglePost';
type Props = {
params: { slug: string };
children: React.ReactNode;
};
// This function will be called and it will generate
// The metadata object for the page when the route is visited
export async function generateMetadata({ params }: Props): Promise<Metadata> {
// Fetch the post data using the slug
// Implement your own custom data fetch logic
// That returns the resource
const post = await fetchSinglePost(params.slug);
if (!post) {
return customMetaDataGenerator({
title: 'Post Not Found | Naijarium',
});
}
// Generate the metadata using the fetched post data
return customMetaDataGenerator({
title: post.title!,
description: ` Created by: ${post.author_username} - ${post.content).slice(0, 150)} + "...Read More`,
ogImage: post.image,
keywords: post.keywords,
canonicalUrl: `https://your-website.com/post/${post.slug}`
});
}
// Export the layout component that returns the nested page (s)
export default function Layout({ children }: Props) {
return <>{children}</>;
}
Robots and SiteMaps
Metadata for your site is great and are helpful for a number of things too. But if you need your page to be indexed and crawled by search engines, then you also need two important files - robots.txt and sitemap.xml.
robots.ts
- Generates a robot.txt file
- Controls search engine crawling
- Defines which pages should be indexed or not
- can block specific web crawlers or allow all.
sitemap.ts
- Generates a sitemap.xml file
- Lists all important pages on your website
- Help search engines understand your site structure
- Can include information about page updates, importance and frequency.
The beauty here is that, NextJS automatically handles the creation of the robots.txt and sitemap.xml files if you place the robots.ts
and sitemap.ts
files in the app directory, thus improving your site's SEO without manual management.
Example robots.ts
file:
import { MetadataRoute } from 'next';
export default function robots(): MetadataRoute.Robots {
return {
rules: {
userAgent: '*', // allow all crawlers
allow: '/', // allow crawling all pages
disallow: ['/api/', '/api'], // don't crawl api routes
},
sitemap: 'https://your-website.com/sitemap.xml',
};
}
Example sitemap.ts
file
import getAllPosts from '@/actions/getAllPosts';
import { MetadataRoute } from 'next';
export default async function sitemap(): Promise<MetadataRoute.Sitemap> {
const baseUrl = 'https://your-website.com';
const posts = await getAllPosts();
const postEntries = posts.map((post) => ({
url: `${baseUrl}/blog/${post.slug}`,
lastModified: new Date(post.updatedAt!),
changeFrequency: 'weekly' as const,
priority: 0.8,
}));
return [
{
url: baseUrl,
lastModified: new Date(),
changeFrequency: 'daily',
priority: 1,
},
...postEntries,
];
}
What's happening here? Now, you can manually list out the pages in your site that you want to build into your sitemap, but if you have a dynamic website (like a blog or e-commerce); you might want to generate the links for the pages based on the resources you have.
You just have to return an array of SiteMap types. This information will be used by the search engines to understand the structure of your web site/app.
A teeny-bitsy bit of a last step
The next-sitemap
package is a tool for Next.js applications that:
- Automatically generates sitemap(s) and robots.txt files
- Supports both static and dynamic sitemaps
- Allows for custom configuration
- Can generate sitemaps for large-scale applications
- Can be integrated into the build process
How to Use it?
Create a next-sitemap.config.ts
file in the root of your project, and include the content below.
module.exports = {
siteUrl: 'https://your-website-url.com',
generateRobotsTxt: true,
};
Update your script object in package.json
to include
{
"name": "your-project-name",
"version": "0.1.0",
"private": true,
"scripts": {
"dev": "next dev",
"build": "next build",
"start": "next start",
"lint": "next lint",
"postbuild": "next-sitemap"
},
// ...other configurations here
}
Conclusion and Errrm..... See ya
Phew! Now you're up and running, you can inspect your website on the developer tools or share a link to your site to see that link previews have kicked in (after deployment of course, not localhost link).
Have fun building, and feel free to reference this guide whenever you need to implement SEO on your NextJS App.
Note: There are many other things required to help your page rank faster too, from page load speed, responsiveness, and relevance of content. But, at least NextJS offers you the option of using the flexibility of ReactJS Components-based UI building, without sacrificing on your site's 'crawlability' by search engines.
Happy Coding, and cheers from Steve!
Additional Resources:
Top comments (5)
Site maps generated and processed easily by Google
It's not just words, I used this guide to implement minor optimization on my side project (and here's the analysis on Google Search PageSpeed Insights) - And thanks to NextJS you don't have to pay subscription to plugin services or "SEO" experts.
Some more work needed on mobile, but it's a good start
Great article and tips Stephen! Thank you for sharing, literally saved me hours of research on my current project!
Thank you Caleb, I'm glad you found it helpful. Happy coding π―