DEV Community

Cover image for Practical Guide to Implementing Functional SEO in NextJS App Router: Static & Dynamic Metadata
Stephen Omoregie
Stephen Omoregie

Posted on

Practical Guide to Implementing Functional SEO in NextJS App Router: Static & Dynamic Metadata

Drum Roll...

First off, no need doing the ReactJS vs NextJS banter. If you're thinking about optimizing your website/web app for:

  1. Search Engines - Crawling and indexing - so your site and content can rank on page search
  2. 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
Enter fullscreen mode Exit fullscreen mode

Open the code editor and start the development server.

npm run dev
Enter fullscreen mode Exit fullscreen mode

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>
  );
}

Enter fullscreen mode Exit fullscreen mode

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).

  1. You can define and export a variable named metadata using Metadata type (for static details) or a function named generateMetadata (this function is especially helpful for loading dynamic data which can then be used to create metadata object for dynamic pages).
  2. Metadata can be defined in layout files and overridden in nested pages.
  3. 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,
  },
};
Enter fullscreen mode Exit fullscreen mode

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
Enter fullscreen mode Exit fullscreen mode

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,
    },
  };
}

Enter fullscreen mode Exit fullscreen mode

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.

Enter fullscreen mode Exit fullscreen mode

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}</>;
}

Enter fullscreen mode Exit fullscreen mode

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',
  };
}

Enter fullscreen mode Exit fullscreen mode

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,
  ];
}

Enter fullscreen mode Exit fullscreen mode

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:

  1. Automatically generates sitemap(s) and robots.txt files
  2. Supports both static and dynamic sitemaps
  3. Allows for custom configuration
  4. Can generate sitemaps for large-scale applications
  5. 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,
};
Enter fullscreen mode Exit fullscreen mode

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
}

Enter fullscreen mode Exit fullscreen mode

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:

  1. How To Submit Your URL To Google Instantly
  2. NextJS - Search Engine Optimization Course
  3. NextJS MetaData Documentation

Top comments (5)

Collapse
 
cre8stevedev profile image
Stephen Omoregie

Site maps generated and processed easily by Google

Site Maps

Collapse
 
cre8stevedev profile image
Stephen Omoregie

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.

Google Page Analysis

Collapse
 
cre8stevedev profile image
Stephen Omoregie • Edited

Some more work needed on mobile, but it's a good start

Screenshot for mobile score

Collapse
 
goldfinger profile image
Caleb

Great article and tips Stephen! Thank you for sharing, literally saved me hours of research on my current project!

Collapse
 
cre8stevedev profile image
Stephen Omoregie

Thank you Caleb, I'm glad you found it helpful. Happy coding πŸ’―